UML与面向对象设计影印丛书 ATL技术内幕 Brent Rector 幺、,英 Chris Sells M 吞 北 京 ATL 足 •紺 小 而 乂 活 的 类 , j •开发COM 拊 件 。本 m A T L 的内部丁作原理做了深入的剖析, 汴细介绍了 X J* A T L 设计的难础知 i只和体系结构, 展小1 了 A T L 如 何 lj C O M 相 匹 配 , 并对•些里要的 实施绌丨v 做 r u 体提不,在此咕础丨: ,对 A T U t ^ 萵 级 内 容 和 劝 能 做 / 免分的介绍。史为 ® ® 的是, 本 15还指出了 A 1 U M : 的 • 些 缺 点 ,并 & 诉读荇如何 1 ;• 避 免 , 本 贿 】• •定的技术深度, 合訂经验的COM 和V C + + W 序员阅读。 English reprint copyrightC2003 by Science Press and Pearson Education North Asia Limited. Original English language title: ATL Internals, 1st Edition by Brent Rcctor and Chris Sells, Copyright©2001 ISBN 0-201-69589-8 All Rights Reserved. Published by arrangement with the original publisher, Pearson tducaiion. Inc., publishing as Addison-Wesley Publishing Company, Inc. For sale and distribution in the People's Republic of China exclusively (except Taiwan, Hong Kong SAR and Macao SAR). 仅 限 j•屮华人K 共 和 闲 境 内(+ 包括中阳呑港、澳门特别行攻K 和中M台湾地区) 销饩发行。 木 IW 丨丨1丨贴打Pearson Education(培 t 教 出 版 集 丨 扑 )激光防伪怵签。尤标签荇小得销 W 7 -: 01-2003-2538 图书在版编目(CIP)数据_________________________________________________________________________ ATL 技术内辂=ATUntemals/ ( 美〉Hi•兑托(Rector,B.)(美) 寒尔斯( S e lls ,C .)著.一影印本. 北t 科7 出版汗,2003 ISBN 7-03-011407-8 I .A... I【.① ② 塞 ... III.软件开发一英义 IV.TP311.52 馆 CIP 数椐核卞. (2003) 第 030875 4 策划编辑:李佩乾/ 责任编辑:李佩乾 责任印制: 吕春珉/封面制作:东方人华平而设计室 漪芩♦廖从出版 北余尔典城权北街丨6兮 邮改tft丨 1少丨00?】7 http:// www. scienccp. com M 凊 邦 鈉 广 印刷 科学出版扑发行各地新华H店经销 * 2003年5 月第- 版 开 本:787 X 960 1/16 2003年 5 月第一次卬刷 卬 张:40 3/4 印数:1 _2 000 字 数 :780 000 定价:70. 0 0 元 ( 如有印装质量问题,我社负责调换〈环伟 > ) 影印前言 随着计算机硬件性能的迅速提高和价格的持续下降. 其应用范围也在不断扩大。交 给 i十算机解决的问题也越来越难,越来越复杂:这就使得计算机软件变得越来越复杂和 庞大:2 0 世 纪 6 0 年代的软件危机使人们清醒地认识到按照T:程化的方法组织软件开发 的必要性 > 于是软件幵发方法从6 0 年代毫尤X 程性可言的手T.作坊式开发,过 渡 到 70 年代结构化的分析设计方法、8 0 年代初的实体关系开发方法,直到面向对象的开发方法。 间向对象的软件开发方法是在结构化幵发范铟和实体关系开发范型的基础.1.发展 来的,它运用分类、封装 、继 承 、消息等人类f t然的思维机制,允许软件开发者处理 史;为复杂的问题域和其支持技术,在很大程度1:缓解了软件危机。面向对象技术发端于 程序设计语言,以肟乂向软件开发的苹期阶段延伸,形成了面向对象的分析和设计c 2 0 世 纪 80年 代 末 90年代初,先后出现了几十种面向对象的分析设计方法。其中, Booch, Coad/Yourdon、OMT 和 Jacobson等 // 法得到了面向对象软件开发界的广泛认可 c 各种方法对许多面向对象的概念的理解不尽相同,即便概念相同,各自技术上的表示法 也不同。通 过 9 0 年代不同方法流派之间的争论,人们逐渐认识到不同的方法既有其容 易解决的问题,又冇其+ 容易解决的问题,彼此之间需要进行融合和借鉴;并且各种方 法的表示也有很大的差异,不利于进一步的交流与协作:在这种情况下,统一建模语言 (UML)于 9 0年代中期应运而生: UML 的产 生 离 不 开 三 位 面 向 对 象 的 方 法 论 专 家 G. Booch、J. Rumbaugh和 I. Jacobson的通力合作。他们从多种方法中吸收f 大遗有用的建模概念,使 UML 的概念 和表示法在规模上超过了以往任何一种方法,并且提供 r 允许用户对语言做进一步扩展 的机制。UML 使不同厂商开发的系统模型能够基于共同的概念,使用相同的表示法, 呈现彼此一致的模塑风格。1997年 11月 UML 被 0 M G 组织正式采纳为标准的建模语 言 ,并在随后的儿年中迅速地发展为事实上的建模语言闽际标准: UML 在语法和语义的定义方面也做了大量的J:作。以往各种关于®向对象方法的 著作通常是以比较简单的方式宛义其建模概念,而以主要篇幅给出过程指导. 论述如何 运用这拽概念末•迸行开发:UML 则以一•种建模语言的姿态出现,使用语言学屮的--些 技术来定义:尽筲« 正从语宵学的角度符它还有许多缺陷,但它在这方面所做的努力却 足以往的各种逑模方法尤法比拟的. 认 UML 的 則 W 版 本 开 始 ,便 受 到 了 计 舞 机 产 业 界 的 重 视 ,0 M G 的 采 纳 和 大 公 m -J 的 支 持 把 它 W I : 了 实 阮 上 的 丨 : 业 标 准 的 地 位 ,使 它 拥 有 越 来 越 多 的 用 户 c. 它 被 广 泛 地 用 ii A T L 技术内幕 _ | ■■ n_ j -------------= m i r ~ r r ~ i n .................................... ni 1 " _ ■ ■ ■_ 1 匕1 • 门 ~ i F* 用领域和多种类型的系统建模,如管理佶息系统、通信句控制系统、嵌入式实时系 统 、分布式系统、系统软件等:近儿年还被运用于软件再r.程 、质量管理、过程管理' fli!K管玴等M l 而R 它的应用不仅f t限于il•算机软件、还可用于-f•软件系统,例如硬 件 设计 、#.务处观流程、企业或亊业单位的结构与行为逮模,等等。 在 UML陆续发布的几个版本中,逐步修正了前一个版本中的缺陷和错误:即将发 布 的 UML2.0版本将是对UML的乂一次乘人的改进将來的UML将甸着语言家族化、 可执行化、精确化等理念迈进,为软件产业的丁.程化提供更冇力的支撑。 本丛书收泌r 与而向对象技术和UML有关的丨2 本书,反映了面向对象技术最新的 发展趋势以及UML的新的研究动态其中涉及对而向对象建模理论研究与实践的有这 样儿本书: 《面向对象系统架构及设计》主要讨论了面问对象的基本概念、静态设计、 永z人对象、动态设计、设计模式以及体系结构等近儿年来面向对象技术领域中的新的坪 论知识与方法; 《用 UML进行用况对象建模》主要介绍了面向对象的需求阶段、分析 阶 段 、设计阶段中用况模铟的建立方法与技术; 《高级用况建模》介绍了在建立用况模 喂中芯要汴意的尚级的问题与技术; 《UML 面向对象设计基础》则侧重于经典的同向 象理论知识的阐述。 涉 及 UML在特定领域的运用的有这样几本:《UML实时系统开发》讨论了迸行实 时系统幵发时需要对UML进行扩胰的技术; 《ffl UM L构 建 Web应用程序》i寸论丫运 用 UML迸 行 Web应用建模所应该注意的技术与方法; 《面向对象系统测W:: 模 喂 、视 m M*T•具 》介 绍了将UML戍用于® 向对象的测试领域所应亭握的方法勺丁.具 ;《对 象 、 构件 、框 架 1'J UM L应 用》讨沦了如何运用UML对面向对象的新技术—— 构件-框架技 术边模的方法策略。 《UML 勺Visual Basic应用程序开发》主要讨论了从UML模期到 Visual Basic程序的逮模与映射方法、, 介绍而向对象编程技术的有两本书: 《COM 高手心经》和 《ATL技术内铪》,深 入探讨了面向对象的编程新技术—— C O M 和 ATL技术的使用技巧与技术内辎 还有-•本 《Executable U M L技术内幕》, 这本书介绍了可执彳T UML 的理念勹其支 持技术,使得模型的验iff•与模拟以及代码的D 动牛成成为可能,也代表苕将来软件开发 的一种新的模式, 总之,这 套 R所涉及的内容包含了对软件生命周期的全过程建模的方法与技术,同 时也对近年来的热点领域建模技术、新型编程技术作了深入的介绍,有些内容已经涉及 到了前沿领域:吖以说,每一本都很经典, 冇鉴于此,特向软件领域中不同程度的读者推荐这套书,供大家阅读、学习和研究 北京大学计算机系蒋严冰博士 Foreword W h e n I first saw th e title of th is book, I told C hris S ells th at it so u n d ed like th e b o o k I alw ays w anted to w rite. E ver since w e released A TL, som e of us have been saying, “I’ve often thought th at I should w rite a book on how A TL w orks.” A fter reading ATL Internals, I d o n ’t th in k th ere w ould be m u ch left for m e to w rite about. A ctually, this is kind o f a relief. A t th is point, I think m o st asp ects of A TL have b een covered, and ATL Internals provides an excellent so u rce of inform ation on the in n er w orkings of ATL. So, C hris asked m e to provide som e inform ation th at can’t be deduced by look­ ing at the A TL source code. A Brief History of ATL I first got into tem plates in late 1995,w hile I w as a developer on the M FC team . A friend of m ine here w as evaluating various STL vendors for the V isual C + + prod­ u ct and he talked a lot about tem plates to m e. I played around w ith tem plates a bit, b u t didn’t do m uch w ith them . S oon after, the VC team split off an enterprise team to focus solely on V isual C + + 4.2 E nterprise E dition (V C E E), our first VC enter­ prise product. I m oved over to head up the libraries w ork for V C EE. A t th e tim e w e explored several different ideas. M icrosoft T ransaction S erver w as ju st getting started then and w e talked a lot w ith them about CO M , transactions, databases, and m iddle-tier business objects. P retty quickly, w e realized that w e needed a better m echanism for creating CO M objects from C + + . Jan Falkin and C hristian B eau­ m ont w ere w orking for m e at that tim e. Jan w as w orking on an autom ation inter­ face to O D B C data sources, and C hristian w as w orking on a tem plate-based access m ethod to O D BC data sources (a forerunner of our current O LED B consum er tem ­ plates). I w as w orking on the CO M infrastructure, since everything w as already pointing to CO M back then. Initially, I w as ju st playing aro u n d w ith C O M an d tem plates, b u t slow ly it started to stabilize around a few im portant concepts. F rom the beginning I w anted to sup­ p o rt all threading m odels, but I didn’t w ant to pay for it unless it w as needed. T he sam e w as true for aggregation. I didn’t w ant anyone to have an excuse not to use A TL. (I d id n ’t w an t to h ear, “W ell,A TL is cool, b u t I can sav e a co u p le o f b y tes if I d o this m yself.” )So, perform ance and flexibility outran ked ease of use w hen th at deci* sion had to be m ade. ATL INTERNALS ATL 1.0 O ne of the m ain concepts th at cam e out of this w as th at the class th at a u ser w rites is n o t the class th at w as actually instantiated. T his allow ed m any optim izations that otherw ise could not have occurred. Som e of the other concepts w ere m ultiple in­ h eritan ce for interfaces, “creato r” functions, and a data-driven C O M m ap. W e started to show this around and got a lot of good feedback on it. Several people thought w e should get this out to custom ers as soon as possible, so w e decided to R TW (release to the w eb) in th e early sum m er of ’96. T hat w as A TL 1.0. O ur w orking nam e for our libraries had been M EC (M icrosoft E nterprise C lasses), but our m arketing person thought w e should have som ething that m ore reflected w hat w e w ere doing. B ecause of our CO M focus and the fact that at the tim e, everything w as being called “A ctive” som ething or other, w e selected the nam e A ctive T em plate Library. ATL 1.1 W e got a good reception for ATL and in late sum m er of ’96 w e released A TL 1.1. B y this tim e,Jan and C hristian had started w orking directly on ATL. A TL 1.1 had bug fixes and support for a few m ore features such as connection points, N T services, R G S registry support, and security. ATL 2.0 and 2.1 A fter A TL 1.1, w e started w orking on A TL 2.0. Its prim ary focus w as the creation of A ctiveX controls. Jan and C hristian did m uch of the w ork on this, w hile I still fo ­ cused on the core stuff (such as rew riting the connection points to m ake them sm aller). N enad Stefanovic also joined us at th at tim e and started w ork on the w in­ dow ing support in ATL, as w ell as doing the com posite control sup p o rt in VC 6.0. W e w ere originally planning on A TL 2.0 to be shipped on the w eb targeting VC 4.2. H ow ever, o u r plans changed and w e shipped A TL 2.0 in VC 5.0 (12/96), and shipped A TL 2.1 w ith th e A lpha version of V isual C + + 5.0. T he only difference betw een A TL 2.0 and 2,1 w ere som e bug fixes for A lpha, M IPS, and Pow erPC . W e also sim ultane , ously shipped ATL 2.1 on the w eb w ith A pp W izard and O bject W izard support for V C 4 . 2 . ATL 3.0 A fter a couple of m onths of w orking on A TL 3.0 (called A TL 2.5 at th e tim e), C hris­ tian and I w ere burned out and took som e tim e off from A TL, w hile Jan took over as ATL lead. A fter a few m onths w e cam e back, and C hristian becam e the ATL lead w hile I m oved on to explore som e o th er things for V isual C + + , although I do still get into the source code every now and then. FOREWORD The Future of ATL W e shipped VC 6.0 in Ju n e ,98 and as I w rite this, w e are currently w orking on the next release. E xpect to see Jots of cool new stuff in A TL as w ell as som e new w ays of accessin g th e A TL functionality. I am glad to see A TL contin u e to evolve, w hile at the sam e tim e m aintaining the original goals of generating sm all, efficient code. So, take a look at this hook, learn som e new tricks, and gain a deeper understanding of how it all w orks. Jim Springfield Inventor of ATL M icrosoft C orporation Preface C is a fram ew ork for generating assem bly language code (ASM ). M ost ASM pro­ gram m ers shifting to C spent a lot of tim e exam ining and replacing the c om piler- generated code because they didn’t trust the com piler to create correct code. A s tim e w ent on and C com pilers got better, ASM program m ers gradually learned to trust the com piler to generate not only correct code, but also efficient code. O f course, talented C program m ers never forget that the com piler is generating A SM . T hey know that they can still reach into selected areas and dictate the generated code w hen their needs exceed the capabilities of the language. M icrosoft's A ctive Tem plate Library (ATL) is a fram ew ork for generating C + -f/C O M code. M ost C + + program m ers shifting to ATL seem to fear exam ining, let alone replacing, A T L -generated code. T hey don’t tru st A TL any m ore than A SM program m ers trusted their C com pilers, because the source for ATL is fairly in­ scrutable to the casual observer. H ow ever, A TL is perform ing the sam e services for the C + -f/CO M program m er that the C com piler perform ed for A SM program m ers, nam ely, providing a sim pler w ay to generate boilerplate code correctly and effi­ ciently. O f course, talented A TL program m ers never forget that A TL is generating C + 4*/C O M code. T hey know th at they can still reach into selected areas an d dictate the generated code w hen their needs exceed the capabilities of A TL O ur goal in w riting this book is to tu rn you into an A TL program m er, unafraid to reach into it and b en d it to y o u r w ill. T his book is for the C + +/C O M program m er shifting to ATL 3.0,as provided w ith V isual C + + 6.0. B ecause A TL w as built w ith a set of assum ptions, to be an ef­ fective A TL program m er you need to understand not only how A TL is built but also w hy. O f course, to understand the w hy of A TL ,you’ll have to understand th e envi­ ronm ent in w hich ATL w as developed, th at is, CO M . Instead of attem pting to com ­ press all required C O M know ledge into one or tw o chapters, this book assum es you already know CO M and show s you the design, usage, and internals of ATL. D on B o x ’s Essential COM (A ddison-W esley, 1998) is a good source of CO M know ledge, if you'd like to bru sh up before diving into ATL. W ith th e exception of the first chapter, this book w as arranged from the low est levels of A TL to the highest, each chapter building on know ledge in previous chap­ ters. T he first chapter is a brief overview of som e of the m ore com m on uses for A TL, an d th e w izards (if they exist) th at aid in th ese uses. W henever things get too ATL 丨 NTERNAI.S d etailed in th e first chapter, how ever, w e refer you to a subsequent ch ap ter th at wiJJ provide m ore in-depth coverage. C hapters 2 through 5 present the core of ATL. C hapter 2 covers the ATL s m a rt types,such as C C om Ptr, C C om Q IPtr, CComBSTR, and C C om V ari an t. C hapter 3 discusses how objects are im plem ented in ATL, con­ centrating on the great range of choices you have w hen im plem enting IU nknow n. C hapter 4 discusses the glue code required to expose COM objects from COM servers. C hapter 5 delves into the im plem entation of IU nknow n Rgain, th is tim e co n ­ centrating on how to im plem ent Q u ery ln terface, show ing techniques such as tear-off in terfaces an d aggregation. C hapters 6, 7、and 8 discuss canned interface im ­ plem entations that ATL provides to support object persistence, connection points, and C O M collections and enum erators, respectively. T hese services can be used by com ponents that m ay or m ay not provide their ow n user interface. C hapters 9 ,1 0 , and 11, on th e o th er hand, co n cen trate on building both standalone applications and user interface com ponents. T hese chapters cover the ATL w indow classes, con­ trols, and control containm ent, respectively. B ecause m uch of w hat m akes the ATL source difficult to read is its advanced use of tem plates, A ppendix A provides a set of exam ples m eant to illustrate how tem plates are u sed and built. If you've seen A TL source code before and w ondered w hy you can p ass the nam e of a deriving class to a base class tem plate, you m ay find A ppendix A useful. A ppendix B provides a list of th e im p o rtan t A TL classes and th e h ead er files in w hich they are d eclared (arid of­ ten im plem ented). W hen w riting these chapters, it becam e necessary not only to show diagram s ai\d sam ple usage code, but also internal ATL im plem entation code. In fact, this book often becom es your personal tour guide through the A TL source code. So that you m ay distinguish author-generated code from M icrosoft-em ployee-generated code, w e’ve adopted the follow ing conventions: // This code is author-generated and is an example of what y o u ,d type. II Bold-faced code requires your particular attention. CComBSTR bstr = 0LESTR("Hello, World."); B ecause the A TL team didn’t w rite their code to be published in book form , it w as often necessary to reform at it o r even to abbreviate it. E very effort h as been m ade to retain th e essen ce of th e original code, but, as alw ays, th e A TL so u rce co d e is th e ftnal arbiter. Y ou m ight find A ppendix B useful w hen tracking dow n the actual im plem entation. If there are any errors in the sam ple source or in any p art of this book, you m ay contact B rent R ector at http://w w w .w iseow l.com and C hris Sells at http:// PREFACE w w w .seIlsbrothers.com . T he source code used in this book is available at http:// www.wiseowl.com/ATLIntemals.htm. T he authors w ould like to thank all of the follow ing for th eir contributions to this book. W hile it m ay have been possible to com plete this book w ithout them , it w ould have been a pale im itation of w hat w e w ere able to achieve w ith their help. C hris w ould like to thank his w ife, M elissa, and his boys, John and T om , for sparing him the countless evenings and w eekends to w ork on this project. C hris w ould also like to th an k B rent R ector for letting him horn in on his book project. B rent w ould like to thank his w ife, Lisa, and his children, C arly and Sean, for delaying the delivery of th is book significantly. If it w eren't for them , h e’d never leave th e com puter som e days. B rent w ould like to thank C hris for his intelligence, patience, and general good looks.1 B rent and C hris w ould like to thank several folks together. Special thanks to C hristian B eaum ont, Jim Springfield, W alter Sullivan, and M ark K ram er for suffer­ ing uur nagging questions and taking the tim e to answ er them . M ore special thanks to D on B ox for his MSJ A TL feature, w hich so heavily influenced the course and, in turn, this book. T hanks to the review ers: D on B ox, K eith B row n, Jo n F landers, M ike F rancis, K evin Jones, S tanley L ippm an, D hanna S hukla, Jim Springfield, Jeff Stalls, Jaganathan T hangavelu, and Jason W hittington. Special thanks goes to D harm a for his especially thorough, and educational, review s. T hanks to fellow D evelopM entor instructor Fritz O nion for his groundbreaking w ork delving into the depths of ATL control containm ent. T hanks to a form er student, V lad an V idakovic, for inspiring C hris to delve a bit m ore into the H TM L control. T hanks to T im E w ald, Jim Spring- field, and D on B ox for their help in developing the forw arding shim s trick. T hanks to the m em bers of the A TL and D C O M m ailing lists,especially D on B ox, Tim E w ald, C harlie K indel, V alery Pryam ikov, M ark R yland, and Z ane T hom as. A nd last, but not least, thanks to A ddison-W esley, especially J. C arter S hanklin and K rysia B ebick, for providing an environm ent, in w hich w e actually w ant to w rite (although not as quickly o r as concisely as they m ight like). W e w ould like to thank all those w ho so carefully read the earlier printings of ATL Internals and found sm all errors, w hich have been corrected in this printing: E lias A ndersson, R uben B artelink, V ince B lasberg, Y ao C ai, Y un C hang, D an C or- p ron,D arron D evlin, M atevz D olenc, Jerry D ungee, Jonathan F landers, N igel F oster, M ichael G erfen, T hom as G oering, K hurram H aroon, R obert D. H errick, Felipe H em as, R obert H ill, A lan K ang, Y u-M ing K ao, R oland K ing, A dam K runiholz, E ric Lam ontagne, Tasch M atthias, G odson M enezes, Jeffrey M etos, H ieu N guyen, A. N onynious, M ichael N ym an, Joe O ’Leary, A lan Parker, G eorge Reilly, K w ak Seoung- T ae,V ivian D e Sm edt, D an Sullivan, Jonathan Sun, Jeroen T hur, A lan W illiam son , and O chi W u. Special thanks to R avi Singh for his suggested im provem ents to the B ullsE ye control so th at it now w orks in a w ider variety of challenging containers. 1 You only get one gu^ss as to who wrote this sentence, and he doesn't have my initials or my good looks. BBR Contents Chapter 1 Chapter 2 Foreword ix Preface xiii Hello, ATL 1 What Is ATL? 1 Creating a COM Server 2 Inserting a COM Class 5 Adding Properties and Methods 11 Implementing Additional Interfaces 13 Support for Scripting 16 Adding Persistence 17 Adding and Firing Events 19 Using a Window 23 Implementing Component Categories 27 Adding a User Interface 28 Hosting a Control 29 Summary 32 ATL Smart Types: Strings, BSTRs, VARIANTS, and Interface Pointers 33 String Data Types, Conversion Macros, and Helper Functions 33 The CComBSTR Smart BSTR Class 43 The CComBSTR Class 45 The ComVariant Smart VARIANT Class 61 The CComPtr, CComQIPtr, and CComDispatchDriver Smart Pointer Classes 74 S u m m a r y 9 0 Chapter 3 Objects in ATL 93 Recall: COM Apartments 93 ATL INTERNALS Implementing IUnknown 95 The Layers of ATL 98 Threading Model Support 99 The Core of IUnknown 107 Your Class 114 CComObject et a丨. 120 ATL Creators 134 Debugging 149 Summary 155 Chapter 4 COM Servers 157 A Review of COM Servers 157 The Object Map and the CComModule Class 159 The Object Map 160 Methods Required of an Object Map Class 163 The CComModule Class 197 CComCoClass Revisited 206 Server Optimization Compilation Options 208 Summary 210 Chapter 5 Interface Maps 211 Recall: COM Identity 211 Table-Driven Query Interface 213 Multiple Inheritance 220 Tear-off Interfaces 231 Aggregation: The Controlling Outer 239 Interface Map Tricks 248 Summary 256 Chapter 6 Persistence in ATL 257 A Review of COM Persistence 257 ATL Persistence Implementation Classes 268 The Property Map 268 The Persistence Implementations 270 Additional Persistence Implementations 285 Adding Marshal-by-Value Semantics Using Persistence 294 . Summary 297 CONTENTS v Chapter 7 Chapter 8 Chapter 9 Chapter 10 Chapter 11 Collections and Enumerators 299 COM Collection and Enumeration Interfaces 299 Enumerating Arrays 313 Enumerating STL Collections 324 Collections 335 STL Collections of ATL Data Types 341 Simple Collections 346 Object Models 349 Summary 354 Connection Points 357 A Review of Connection Points 357 Creating an ATI-Based Connectable Object 361 Creating an Object That Is an Event Recipient 373 How It AH Works: The Messy Implementation Details 386 Summary 405 Windowing 407 The Structure of a Windows Application 407 CWindow 410 CWindowlmpi 416 CDialoglmpl 452 Windows Control Wrappers 462 CContainedWindow 468 Summary 474 ActiveX Controls 475 A Review of ActiveX Controls 475 The BullsEye Control Requirements 477 Creating the Initial Control Using the ATL Wizard 486 The Initial BullsEye Source Files 489 Developing the BullsEye Control Step by Step 495 Summary 538 Control Containment 539 How Controls Are Contained 539 Basic Control Containment 540 Hosting a Control in a Dialog 573 ATL 丨NTERNALS Composite Controls 581 HTML Controls 585 ATL's Control Containment Limitations 598 Summary 599 Appendix A C ++ Templates by Example 601 The Need for Templates 601 Template Basics 603 A Different Kind of Polymorphism 605 Function Templates 611 Member Function Templates 612 Summary 613 Appendix B ATL Classes and Headers 615 Index 623 CHAPTER 1 Hello,ATL W elcom e to the A ctive T em pJate L ibrary (hereafter referred to as ATL). In this chapter, I present a few of the task s th at I expect you’ll w ant to perform using A TL and the integrated w izards. T his is by no m eans all of w hat ATL can accom plish nor is it m eant to be ex h au stiv e coverage of th e w izards o r th eir output. In fact, th e rest of this book is focused on how ATL is im plem ented to provide the COM (C om po­ nent O h jert M odel) glue t hat holds this ex am p le to g eth er (as w ell as sev eral o th ers). T his chapter is really ju st a w arm -up to get you fam iliar w ith the su p p o rt th at the V isual Studio environm ent provides the ATL program m er. What Is ATL? E xpanding the acronym really doesn't describe very w ell w hat ATL is or w hy w e have it. T he Active part is really a residue from the m arketing age at M icrosoft w hen A ctiveX 1 m eant all of CO M . A s of this w riting (and it’s bound to change again), A ctiveX m eans controls. A nd w hile ATL does provide extensive support for build­ ing controls, il provides m uch m ore than t hat. What ATL Provides 1. C lass w rappers around high-m aintenaiice data types such as interface pointers, VARIANTS, BSTRs, and HWNDs. 2. C lasses that provide im plem entations of basic CO M interfaces such as IU n­ known, IC lassF actory, ID ispatch, IP ersistX xx, IC onnectionP oint- C ontainer, and IEnum Xxx. 3. C lasses for m anaging CO M servers, that is, for exposing class objects, self­ registration, and server lifetim e m anagem ent. 4. W izards to save you typing. 1 The original cxpansiiion of ATL was the ActiveX Template Library. AT L INTERNALS What You Provide ATL w as inspired by th e cu rren t m odel cit izen in the w orld of C + 十 class libraries, the S tandard T em plate L ibrary (STL). L ike STL, ATL is m eant to be a set of sm all, efficient, and flexible classes. H ow ever, w ith pow er com es responsibility. So, like STL, ATL can only be used effectively by an experienced C + + program m er (a little STL experience is valuable, too). O f course, since w e’re going to be program m ing C O M , experience using and im ­ plem enting C O M objects and servers is absolutely required. F or those of you hop­ ing for a C O M -know ledge-free w ay to build your CO M objects, ATL is not for you (n o r is V isual B asic, V isual J + + ,o r an y th in g else, for th at m atter). In fact, using A TL m eans being intim ately fam iliar w ith C O M in C + + as w ell as w ith som e of the im ­ p lem entation details o f A TL itself. Still, ATL is packaged w ith several w izards that are helpful for generating the initial code. In the rest of this chapter, I present the various w izards available for ATL program m ers as of V isual C + + 6.0. Feel free to follow along. Creating a COM Server Running the ATL COM AppWizard T he first step in any V isual C + + developm ent endeavor is building the w orkspace and the initial project. For ATL program m ers, this m eans choosing the ATL CO M A ppW izard from the P rojects tab of the N ew dialog, as show n in Figure 1.1. N ote that the A TL C O M A ppW izard is selected by default. That, is because it is the m ost im portant kind of pro ject you can create using th e V isual S tudio (it's ju st a coinci­ d en ce th at it’s also alp h ab et ically first). T he n an ie of th e p ro ject (sh o w n in the figure as Pi S v r) w ill be the nam e of your generated D LL or EX E server. T he job of the A TL CO M A ppW izard is to build a project for your CO M server. A CO M server is either a dynam ic link library (D LL) o r an executable (EX E). F ur­ ther, the EX E can be a standalone application or an N T service. The ATL COM A ppW izard su p p o rts all three of these server types, as show n in F igure 1.2. T he ATL CO M A ppW izard also provides three other options. The first allow s you to bundle your custom proxy/stub code w ith your D LL server. R egardless of the kind of server you choose, ATL w ill provide a < p ro jectn am e> p s .m k m akefile for use w hen building your custom proxy/stub. T his file is for building a separate proxy/stub D LL to distribute to all client, and server m achines th at need to be able to m arshal and unm arshal your custom interfaces. If you'd like to bundle the proxy/stub DLL into the server DLL (and save yourself a separate proxy/stub DLL on the server m achine), you m ay check (he “A llow m erging of proxy/stub code” op- HELLO. ATL RE3 F«o> Ptoiocit | C C M A p o W t Woktpaoes | Othai Ooojnonks £i ATL COM ArxMzvd Wn32 Lfcraiy ~y1 Clustcf Resource Typc Wizard J j Custom AppWizafd Database Proiect ^ DevSfudio Add in Wizard i Extended Stored Proc Wizard ISAPI Extension Wizard Makefile jjlS MFC ActiveX CortroN^izard ijM FCAppW ^d (dlj 5 J MFC AppWeacd (exe) New Database Wizafd T l UtAy Pfo»ect "? l Win32 Application *"1W n32 Console Appkation W n32 Dy^amc Unk Lbrary Piojacioamt: -v t: jPiSvr Logatiort______ jd VadbookVsrcVPiS' Wn32 -iJ 3 Cancfll F igure 1.1 • C reating a new V isual S tudio project tion. D oing so w ill put a bunch of conditionally com piled statem ents into your server code to allow m erging the proxy/stub code. To actually merge the proxy/ stu b code, you m u st also perform the follow ing step s (after the w izard g en erates the p r o j e c t ) . 1. A dd _M ERCE_PROXYSTUB to the prep ro cesso r definitions fo r all configurations in th e P ro ject S ettings dialog. 2. U ncheck the “E xclude file from build” option in the settings for d lld a ta x .c. The dl 1 d a ta x . c file is really just a w n ^p er that brings d ll d a ta , c and < p ro j ectn am e> _ P i • c into th e pro ject properly. 3. C hange the precom piled header settings for d ll d a ta x . c to ttN ot using pre­ com piled headers." The second ATL COM A ppW izard option allow s you to use M FC (M icrosoft F oundation C lasses). Frankly, you should avoid this option. T he follow ing are a few com m on objections that I have heard about w hy developers believe that they should use M FC w ith an ATL server. ATL INTERNALS DE3 T h i s ATL project w ttw ii any W * C M K l Weard creates an ATL proiod • COM objects. After con^MnQ this the New ATL Obied command from H wh to specify ihe to e of obied y use Cla»VioM to specify Jhe type of Bu» to add to this proiect youv#ould rSefver Type - ^ C y n a i w c U r^ U br«yR )ai £xecu^ble (EXE) Service (EXE) AIod# metgiig of firo fi/被ub code S(4)portM FC S<4]poftM XS < flack | v ' { finith "| Caned ( Figure 1.2. C r e a t i n g a new ATL COM server 1. “I can’t live without CString (o r CMap, C L is t, e tc .)•” The MFC u t i l i t y classes w ere built as a stopgap until the C + + standards com m ittee defined a standard library. T hey’ve done it, so w e can stop using the M FC versions. T he classes ( s tr i ng, m ap, 1 i s t, etc.) provided in STL are m ore flexible and m ore ro b u st than their M FC equivalents. 2 . ul can’t live without the wizards.” T his ch ap ter is all ab o u t th e w izards pro­ vided by the V isual S tudio for A TL program m ers. A s of V isual C + + 6.0, th e A TL w izards are nearly as extensive as those th at generate M FC code. 3 . “I already know MFC and I can’t learn anything new.” Luckily, none of th ese people are reading this book and they’re either perfectly happy w ith, or blissfully ig n o ran t of, th e 973K D LL th ey ’re using.2 T he third A TL C O M A ppW izard option, S upport M TS, does tw o thii>gs. F irst, it changes som e com piler and linker settings so that M icrosoft T ransaction Server wofkcpace ATL C O M AppWizaid • Step Set as A t i e w O a i N e w & T L Fold Docking Hide Prflpoitie Cl. ■ 馨 •J 2To be fair, it’s possible to statically link with MFC, too, but the minimum overhead for such an action still seems high at roughly 260K. HELLO. ATL (M TS) has the m odifications it needs in the proxy/stub D L L T his saves a bunch of tedious hand tw eaking. S econd, the w izard adds a custom build option so th at w hen the server is built, m tx re re g .ex e is run. T his w ill cause M TS to reacquire CO M servers and save you from refreshing your M TS packages. T hose of you w ho have ever rebuilt an M TS server and suddenly found it no longer running in M TS w ill ap­ p reciate th is feature. Results of the ATL COM AppWizard W ith or w ithout these three options, every COM server generated by the ATL CO M A ppW izard w ill support the three jobs of every CO M server, nam ely, self­ registration, server lifetim e control, and exposing class objects. A s an additional convenience, the w izard adds a custom build step th at registers the CO M server on each successful build. This step w ill either run re g sv r3 2 .ex e < p ro je c t> .d ll or < p ro ject> .exe /re g se rv e r, depending on w hether it is a DLL or EX E server For m ore inform ation about ATL’s support for the three jobs of every COM server as w ell as how you can extend it for m ore advanced concurrency and lifetim e needs, see C hapter 4. Inserting a COM Class Running the ATL Object Wizard O nce you have an A TL C O M server, you,ll probably w ant to insert a new C O M class. T his can be done w ith the N ew ATL O bject item in the Insert m enu. I should note th at the A TL team got th e term inology w rong here. Y ou’re n o t going to be inserting a n object, th at is, an in stan ce of a ty p e 一 y ou’re going to be in sertin g a class, t h a t i s , th e definition of a type. Still, w hile the term inology is w rong, at least they’re con­ sisten t ab o u t it. In future ch ap ters, w e’ll discuss th e object m ap, w hich is all about c l a s s e s . 3 W hen inserting an A TL class, y ou’ll first have to choose the type of class you'd like, as show n in F igure 1.3. If y o u ’re follow ing along, you m ay w an t to tak e a m o­ m ent and explore the various types of classes that the A TL O bject W izard know s about. E ach of these classes w ill result in a specific bunch of code being gener­ ated, using the A TL base classes to provide the m ajority of the functionality and then generating the skeleton for your ow n custom functionality. T he ATL O bject W izard is really your chance to decide w hich interfaces you’d like your C O M class 3Chribtiaii Beaujnont, the development lead for ATL, assures me that he knows the difference between class and object but that Visual Studio already had n Class Wizard (for generation of MFC code), and he didn’t want to confuse things. ATL INTERNALS ATL Ob|ect Wi?aid Qbjecti 2 2 Contfote MisceBaneous Da^a Access 者 1 € 1 1 Add-in Ob(ect Internet Expkx... 3 Q ActiveX Server Component MMC Snapln MS Transach... d | C m « i . Figure 1.3. Inserting a new ATL CO M class to im plem ent O f course, the w izard w on’t provide access to all the functionality of A TL (or even m ost of it), but the generated code is designed to be easy for you to add o r su b tract functionality after the w izard has gotten you started. E xperim enta­ tion is th e b est w ay to get fam iliar w ith th e v ario u s w izard-generated class ty p es and th eir options. O nce you’ve chosen one of the class types (and pressed O K ), the w izard w ill generally ask for som e specific inform ation from you. W hile som e of the classes have m ore options than the Sim ple O bject (as selected in Figure 1.3), m ost of the C O M classes require at least the inform ation show n in F igures 1.4 and 1.5. T he N am es tab of the A TL O bject W izard P roperties dialog really only requires you to type in the sh o rt nam e, fo r exam ple, C al c P i. T his sh o rt nam e is u sed to com - le rest of the inform ation in this dialog (w hich you m ay override, if you T he inform ation is broken into tw o categories. T he necessary C + + infor­ m ation is the nam e of the C + + class and the nam es of the header and im plem enta­ tion files. T he necessary C O M inform ation is the co cl a ss nam e (for the Interface D efinition L anguage [ID L]), th e nam e of th e d efault interface (also fo r th e ID L ), th e friendly nam e, called th e T ype (fo r th e ID L an d th e reg istratio n settin g s), an d finally th e version-independent program m atic identifier (for th e registration settings). T he versioned ProgID is ju st th e version-independent FrogID w ith the “.1” suffix. T he A ttributes tab is your chance to m ake som e low er-level CO M decisions. T he T hreading M odel settin g describes th e kind o f ap artm en t in w hich you’d like in­ stances of this new class to live: SingleT hreaded A partm ent (STA )— w hich is also know n as the A partm ent m odel or a M ultiT hreaded A partm ent (M TA ) 一 k n o w n a s th e F ree-T hreaded m o d el T he Single m odel is for th e rare class th at requires all its objects to share the sam e ^ artm e n t, regardless of the client’s 叩 artm ent type. T he p o s e t h e c h o o s e ) . HELLO. ATL Wi/«Md P»opeit»e* CCalcPi CEPFte CalcPi h CalcPi cpp COM :J c 3 c P i ;|f c d I c p T Jyp^ jCafcPi Class ProgiD: f ! ^ •_ . »•丨 PiSvr CalcPi n o C a n c e l Figure 1.4. Setting CO M class nam es ATL Ob|ecl Wi/aid Piopeities n o N « e s 钱 THr^dno Modal a l n t o r t « o e ^ C u s t o m A g g r e o a k m r no r Qr^ i P $m poiti$upport£noili^' P* Support ComobonCbaiU 「 ftee ThteadadM iuhalw n l-g ' ; C M e i r Figure 1.5. S etting C O M class attributes B oth m odel is for o b jects th at you w an t to live in th e sam e ap artm en t as th eir clients to avoid the overhead of a proxy/stub pair. T he T hreading M odel setting you choose w ill determ ine the value for the T h read i ngModel nam ed value in your in-process serv er self-registration settings as w ell as ju st how thread safe you need your ob­ ject's im plem entation of A ddR ef and R el e a se to be. T he Interface setting allow s you to determ ine the kind of interface you,d like th e class’s d efau lt in terface to b e : custom (it needs a custom proxy/stub and w ill not derive from ID i sp a tc h ) or dual (it w ill use th e typelib m arshaler and w ill derive from ID i sp a tc h ). T his setting w ill determ ine how the ID L that defines your default interface is generated. T he A ggregation setting allow s you to determ ine w hether you’d like your ob­ jects to be aggregatable o r not, th at is, w h eth er to participate in aggregation as the controlled inner. T his setting does not affect w hether objects of y o u r new class can use aggregation as the controlling o u ter o r not. S ee C hapter 3 for m ore d etails ab out being aggregated and C hapter 5 about aggregating other objects. T he S upport IS upportE rrorlnfo setting directs the w izard to generate an im ple­ m entation of IS u p p o rtE rro rln fo . T his is necestiary if you'd like to throw COM exceptions. CO M exceptions (also called CO M E rror Inform ation objects) allow you to pass m ore detailed error inform ation across languages and apartm ent boundaries than can be provided w ith an HRESULT alone. T he Support C onnection Points setting directs the w izard to generate an im ­ plem entation of I C o n n ect! onP oi n t, w hich allow s your object to fire events into scripting environm ents such as those hosted by Internet E xplorer. C onnection po in ts are also used by co n tro ls to fire events into control containers. T he H elp inform ation for the F ree T hreaded M arshaler setting reads as follow s : C reates a free threaded m arshaler object to efficiently m arshal interface point­ ers betw een threads in the sam e process. W ith a ringing endorsem ent like that, w ho w ouldn't w ant to choose that option? U n­ fortunately, the Free T hreaded M arshaler (FTM ) is like an item in an expensive s t o r e : If you have to ask, you can 't afford it. S ee C hapter 5 for a description of the F T M before checking this box. Faking out the ATL Object Wizard S om etim es you’ll find th at you’d like to ru n the A TL O bject W izard inside a project that w asn’t generated by the A TL CO M A ppW izard and th at m ay not even be a CO M server. F or exam ple, it's com m on to w ant to insert a D ialog or an O LED B D ata C on­ sum er into a standalone W in32 application. If the O bject W izard doesn’t find the pieces of the p ro ject th at it n eed s to gen erate th e code, it w ill let you know w ith the m essage box show n in Figure 1.6. If, like m e, you take this m essage as a personal challenge rath er thaxi an actual barrier, you’re going to w ant to know how to m ake it go aw ay. T ow ard th at end, here are the m inim um requirem ents that the ATL O b­ ject W izard, as o f V isual C + + 6.0, is looking for.4 4Thanks to the ATL mailing list gang for working together to figure out these requirements. Lord knows they're not documented HELLO, ATL A l l Ob|RC O Curr«ntfy ATL object may only (E>€. Control Regular OLL) be added to ATL project*, or F igure 1.6. ATL O bject W izard error m essage box ■ T he project m ust be a DLL or a W in32 application. It m ay not be a console application. ■ T he follow ing code m ust appear in the < p ro je c t> . cpp file: BECIN_OB〕ECT_MAP(ObjectMap) END_0B3ECT_MAP() ■ T here m ust, be a < p ro je c t> . i d l file as p art of the project (although it m ay be m arked “E xclude file from build” in th e settings) and the follow ing m inim um library block m ust be present (the com m ents are not necessary): II Library does not need a [uuid] attribute library LIB // Library can be named anything, but it does need a name { } ■ If the integrated developm ent environm ent (ID E ) still gives you trouble, close the w orkspace, rem ove the . neb file, reload the w orkspace, and try again. In th e ev en t th at th is still d o esn ’t help, w ell, y o u tried ________ W arning: Please be aw are that by faking out the ATL O bject W izard, you are circum ­ venting the problem that the ATL O bject W izard is trying to save you from , nam ely, th at you don't have th e right su p p o rt from th e project to actually build the code th at th e w iz­ ard w ill g en erate for you. So, if you’re going to use this technique, you had b etter becom e fam iliar w ith the A TL source so you can m anually add the appropriate . h and . cp p files to std a fx . h and s td a fx . cpp, respectively. A ppendix B show s you w hat ATL header files contain w hich classes. Results of the ATL Object Wizard A fter you have filled in th e necessary options, th e A TL O bject W izard w ill generate a new C + + class, derive it from the appropriate interfaces and ATL interface im plem entations, and put it into a new header file. It w ill also give you a C + + ATL INTERNALS im plem entation file for you to add your custom code. T he C+ + class w ill derive from som e ATL base classes and your new interface and w ill provide a COM 一 M A P th at lists all th e in terfaces th at y o u r object exposes. A n exam ple follow s. // Shaded code generated by ATL wizard class ATL-NO^VTABLE CCalcPi : public CComObjectRootEx• public CComCoClass, public ISupportErrorlnfo, public IConnectionPointContainerlmpl, public IDispatchlmpUICalcPi, AIID.ICalcPi, &LIBID一PISVRUb> { public: CCalcPi() { } DECLARE.RECISTRY^RESOURCEIO(IDfL,CALCPI) BECIN^COM_MAP(CCalcPi) COMJtNTERFACEJENTRY(ICalcPi) COH.INTERFACE.ENTRY(IDispatch) COH^INTERFACE.ENTRYCISupportErrorlnfo) COM.INTERFACE.ENTRYCIConnectionPointContainer) END.COM.MAPC) BECIN.C0NNECTI0N^P0INT^4AP(CCa1cPi) EN0_C0NNECTI0N,P0INTJ4AP() // ISupportsErrorlnfo STOHETHOO(InterfaceSupportsErrorInfo)(REFIIO riid); // ICalcPi public: F or m ore inform ation about the base classes th at A TL uses to im plem ent basic CO M functionality and how you can leverage this im plem entation for building ob­ ject hierarchies and properly synchronize m ultithreaded objects, see C hapter 3. F or m ore inform ation about how to m ake full use of th e C0M _M AP, see C hapter 5. HELLO, ATL Adding Properties and Methods O ne of the things th at m ake a C + + program m er’s life hard is the separation of the class definition (usually in th e .h file) and the class im plem entation (usually in the .cpp file). T he reason th at this can be a pain is the m aintenance required betw een the tw o. A ny tim e a m em ber function is added in one, it has to be replicated to the other. M anually, this can be a tedious pro cess and is m ade even m ore tedious if the C + + C O M progam m er m ust start the process in the . i d l file. D on’t get m e w rong. I love IDL. A language-independent representation of interfaces and classes is the only w ay to go. H ow ever, w hen I'm adding p roperties and m ethods to m y interfaces, I,d like it if m y C + + developm ent environm ent could translate the ID L into C + 十 and drop it into m y . h and . cp p files for m e, leaving m e a nice place to provide m y im plem entation. T hat’s ju st w hat V isual C + + provides. B y right-clicking on a C O M interface in the C lass V iew , you can choose to add a new property o r m ethod. F igure 1.7 show s the dialog th at allow s you to add a prop­ erty to a CO M interface. N otice the Im plem entation box in F igure 1.7. It show s the Acid Propeilv to Inteifdce fietumType: P(rjfe*tyX ypc jlong Property H e ? Caned A ttribute!. | d ^ PareiQ eteis: Function T | p 8 ----------------------------- P fiel Function P Cut Function ^ PfSpPW C PropPytRef lpwpQ0t W M prtngTpw peH y O iJ?! HBESULT DigiNgoar. ratvaQ long "pVaQ; Iprapput id(U 0_ 飞 HRESULT Digls6n] iongrw^aQ: Figure 1.7. A dding a property ATL INTERNALS resu ltan t ID L b ased on th e options as set in th e dialog. T he ID L w ill be u p d ated and the appropriate C + 十 code w ill be generated w hen you click th e O K button. T he fol­ low ing shaded code show s the im plem entation skeleton generated by the w izard. W e have only to provide the appropriate behavior (show n as nonshaded code). STDMETHODIMP CCalcPi::get«Digits(long *pVal) { *pVal ■ nunD igits; r e t u r n S 一 OK; STDMETHODIMP CCalcPi::put«Digits(long newVal) ifC newVal < 0 ) return Error(L"C an’t calculate negative d ig its of PI " ); nuiD igits * newVal; return S.OK; > Sim ilarly, w e can add a m ethod by right-clicking on an interface in the C lass V iew and choosing A dd M ethod. F igure 1.8 show s the A dd M ethod to Interface dialog. A gain, th e w izard w ill update the ID L file (using the ID L show n in th e Im ­ plem entation box), generate the appropriate C + + code, and place us in the im - Add Method to Inteiface 1 1 1 5 F H n T E I (■ ■ ■ ■ _ _................. — ■ |((out, retval] BSTR" pbstrPi) he^ngfn^hodC alcPra % H A CSU ltCairf^oU .1w tv^eSTR-pbX iPa Figure 1.8. A dding a m ethod HELLO, ATL plem entation skeleton to do our job. T he shaded code in the follow ing is w hat re­ m ains of the w izard-generated C + + code after I added the code to im plem ent the m e t h o d . STDMETHOOIMP CCalcPi::CalcPi(BSTR *pbstrPi) { 。 ' # • 鑛 _^SSERTE(m_nDigits >= 0); if( m_nDigits ) { //create buffer for "3." and up to 8 more digits than asked for *pbstrPi * SysAllocStringLen (OLESTR ("B."), m_nDigits + 10); if( *pbstrPi ) { fo 「( int i =0; i < m_nDigits; i +- 9 ) { long nNineDigits = NineDigitsOfPiStartingAt(i +1); swprintf(^pbstrPi + i+2, L"%09d", nNineDigits); // Truncate to number of digits (*pbstrPi)[m_nDigits+2] = 0; else { ^pbstrPi = SysAnocString(L"3"); return *pbstrPi ? S_0K : E_0UT0FMEM0RY; •: - A : * • , -命 - F or a description of CO M exceptions and the ATL E rro r function (used in the put_D i gi ts m em ber function), see C hapter 4. Implementing Additional Interfaces Interfaces are the core of CO M , and m ost CO M objects im plem ent m ore than one. E ven the w izard-generated Sim ple O bject show n earlier im plem ents four interfaces (one custom interface and three standard interfaces). If you w ant your A T L-based 14 ATL IN'ERNALS C O M class to im plem ent an o th er interface, you m u st first, have a definition of it. F or e x a m p l e : interface IAdvertiseMyself : IUnknown { [helpstring("method ShowAd")] HRESULT ShowAd(BSTR bstrClient); } ; If you’re going to im plem ent it, yo u ’re going to need a C + + h ead er file th at de­ scrib es the interface and yo u ’re going to have to do a little b it of typing to add the interface and its m ethods to your class: class ATL_NO_VTABLE CCalcPi : publ i c CCoFn0bjectRootEx, public CComCoClass, public IAdvertiseMyself { • • • BEGIN 一 COM 一 MAP(CCalcPi) • 参■ COM一INTERFACE一ENTRY(IAdverti seMysel f) ENO_COM_MAP() II IAdvertiseMyself STDMETHOD(ShowAd)(8STR bstrClient); } ; O f course, once you’ve added these lines, you still have to im plem ent the m ethods. STDMETHODIMP CCalcPi::ShowAd(BSTR bstrClient) { CComBSTR bstrCaption = OLESTRC'CalcPi hosted by ,•); bstrCaption += (bstrClient SA *bstrC1ient ? bstrClient : OLESTR ("no one"); CComBSTR bstrText = OLESTR("These digits of pi brought to you by C a l c P i ! M ) ; USES„CONVERSION; MessageBox(0, 0LE2CT(bstrText)t 0LE2CT(bstrCaption), MB_SETFORECROUND); return S—OK; HELLO. ATL 15 a t a Cancel A ddTiip««b * ■ _ _ __ _ _ _ _ _ _ _ _ Figure 1.9. Im plem ent Interface dialog If you happen to have the interface you’d like to im plem ent defined in a T ype- Lib, you can use the Im plem ent Interface w izard to insert the skeleton code for you. Y ou can access the Im plem ent Interface w izard by right-clicking on the class and choosing Im plem ent Interface. T he Im plem ent Interface dialog (Figure 1.9) show s ail the custom or dual interfaces in the currently selected TypeLib that aren't already im plem ented by the class in question. B y default, the w izard show s the T ypeL ib associated w ith the current project,5 but you can choose any TypeL ib by pressing the A dd TypeLib button. U nfortunately, as of this w riting, the Im ple­ m ent Interface dialog does not support interfaces that don’t exist in TypeLibs, w hich leaves out m ost of the standard CO M interfaces, such as IP e rsi S t, IM ar- sh a l, and 101 eltem C o n tai n er. O ne hopes that future versions w ill address this deficiency. A fter you’ve chosen to im plem ent an interface, the w izard derives the class from the interface, adds the interface to the C0M _M AP, and puts the m ethod signa­ tu res into the class (as w ell as som e helpful skeleton code). For m ore inform ation about the various w ays that ATL allow s your COM classes to im plem ent interfaces, see C hapter 5. For m ore inform ation about CComBSTR and the string conversion routines used in the ShowAd m ethod, see C h a p t e r 2 . 5 If you're going to implement an interface newly declared in your project’s IDL, make sure to rebuild the TypeLib before using the Implement Interface wizard. ATL INTERNALS p u b lic : BEGIN.COM.MAP(CCalcPi) COM»INTERFACE^£NTRY(ICalcPi) Support for Scripting A ny tim e you run the A TL O bject W izard and choose D ual as the interface type, A TL w ill generate ID L for the default interface that derives from ID i sp a tc h and is m arked w ith the d u al attribute, like so : t object, uuid(OEC22F36-0078-1101-97FD-006008243C8C), dual, helpstring(MICalcPi Interface"), pointer一default(uni que) ] interface ICalcPi : IDispatch B ecause it derives from ID i sp atch , our dual interface can be used by scripting clients, for exam ple, A ctive S erver Pages (A SP) and Internet E xplorer (IE ). ATL provides an im plem entation of the four ID i sp atch m ethods in the ID i sp a tc h - Im pl base class : template class ATI〜 NO—VTABLE IOispatchlmpl : public T To support scripting environm ents, our ATL CO M class derives from ID is- p atch lm p l and adds ID i sp atch to the COM 一 MAP, as show n here. c la s s ATL_NO.VTABLE CCalcPi :... public IDI spa tchlnpl《ICalcPi, &IID^CalcPi, »LIBID一 PISVRLib> HELLO. ATL COH_JNTERFACE.ENTRY(IDispatch) COH_INTERFACE_ENTRY(ISupportErrorInfo) COH_INTERFACE_ENTRY(IConnectionPointContainer) END_COM_MAP() A s you have no d oubt already noticed, by sim ply choosing D ual as the interface type in the A TL O bject W izard, the w izard w ill provide all the im plem entation code ju st show n as w ell as the interface definition. O nce our CO M class su pports ID i s - p atch , w e can use objects of that class from scripting environm ents. H ere’s an exam ple H TM L page that uses an instance of the C al cP i object: F or m ore inform ation about how to handle the inconvenient d ata types associ­ ated w ith scripting, nam ely, BSTRs and VARIANTS, see C hapter 2. Adding Persistence ATL provides base classes for objects that w ish to be persistent, th at is, saved to som e persistent m edium (like a disk) and then restored later. CO M objects expose this support by im plem enting one of the CO M persistence interfaces, such as IP e r­ si stS tream ln it, IP ersistS to rag e, IP ersistP ropertyB ag. ATL provides im plem entation of these three persistence interfaces, nam ely, IP e rsi stS tre am - In itlm p l, IP ersi stS to rag elm p l, and IP ersi stP ropertyB aglm pl. By deriv­ ing from any of these base classes, adding a data m em ber called m _bR equi resS av e ATL INTERNALS expected by each of these base classes, and adding the nam e of the interface to t h e C O M 一MAP, your C O M object w ill support persistence. class ATL_NO^VTABLE CCalcPi : public CComObjectRootEx, public CComCoC1ass, public IDispatchImpl, public IPersistPropertyBagImpl { public: BEGIN_COM_MAP(CCalcPi) C0M_INTERFACE_ENTRY(ICa1cPi) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE^ENTRYCIPersistPropertyBag) END_COM_MAP() II ICalcPi public: STDMETHOOCCalcPi)(/*[out, retval]*/ BSTR* pbstrPi); STDMETHOO(get_Digits)(/*[out, retval]*/ long *pVal); STDMETHOD(put_Digits)(/*[in]*/ long newVal); public: BOOL m_bRequiresSave; // Used by persistence base classes private: long m_nDigits; H ow ever, th at’s n o t quite all th ere is to it. A TL’s im plem entation of persistence needs to know w hich parts of your object need to be saved and restored. F or that inform ation, ATL’s im plem entations of the persistent interfaces rely on a table of object properties th at you w ish to persist betw een sessions. T his table is called a PROP 一MAP and contains a m apping of property nam es and dispatch identifiers (as defined in th e ID L). So, given the follow ing interface, interface ICalcPi : IDispatch { [propget, id(l)] HRESULT Digits([out, retval] long ^pVal); HELLO. ATL [propput, id(l)] HRESULT Digits([in] long newVal); [id(2)] HRESULT CalcPi([out, retval] BSTR* pbstrPi); T he PROP_M AP w ould be contained inside o u r im plem entation of IC al cP i like so: class ATL—N0_VTABLE CCalcPi :... { • • • public: BECIN_PROPJ^AP (CCal cPi) PROP^ENTRYC'Digits", 1, CLSID 一 NULL) END.PROP^MAPC) G iven an im plem entation of IP e rsi stP ro p erty B ag , our IE sam ple code can be expanded to su p p o rt initialization of o bject p ro p erties via persistence using the < param > tag like so: F or m ore inform ation ab o u t ATL’s im plem entation of persistence, see C hapter 6. Adding and Firing Events W hen som ething interesting happens in a C O M object, w e’d like to be able to spon­ tan eo u sly notify its clien t w ith o u t th e client polling th e object. C O M p ro v id es a stan ­ d ard m echanism for sending these notifications to clients (norm ally caMedfiring an event) using the connection point architecture. ATL 丨 INTERNALS C onnection point events are really ju st m ethods on an interface. T o su p p o rt the w i d e s t variety of clients, an event interface is often defined as a d i sp i n te rfa c e . C hoosing Supports C onnection Points in the ATL O bject W izard w ill generate an event interface an d publish it as th e default so u rce interface in y o u r class. T he fol- low ring is an exam ple of the w izard-generated code augm ented w ith a single event m ethod (show n in bold): dispinterface _ICalcPiEvents { properties: methods: void 0nDigit([1n] short nlndex, [in] short nDiglt); coclass CalcPi { [default] interface ICalcPi; [default, source] dispinterface _ICalcPiEvents; O nce the event interface has been defined, and the server’s < p ro je c t> . tlb has been built, right-clicking on the class in the C lassV iew and choosing Im ple- m ent C onnection P oint w ill display the dialog show n in F igure 1.10. Selecting an event interface in this dialog (and pressing O K ) w ill cause the w izard to g e n e r a t e a w rapper class (called a eonnection point proxy) for firing m ethods to interested c l i e n t s : template class CProxy^ICalcPIEvents : public IConnect!onPointlmpl { //Warning this class may be recreated by the wizard, public: VOID F1rcL.0n01git(SM0RT nlndex, SHORT nOigit); B ecause th e w izard w ill also generate co d e so th at th e class o f objects firing these ev en ts derives from th e P roxy class, th e class is free to u se th e F i re m ethods any- HELLO. ATL Implemenl Connection Poinl PISVRUb J Ete name: JPiSvrCPh gfowto Inteifocas 0 JCalcPiEvenls Figure 1.10. Im plem enting a connection point STDMETHODIMP CCalcPi::CalcPi(BSTR *pbstrPi) { // (code to calculate pi removed for clarity) // Fire each digit for( short j * 0; j !: runDigits; ++j ) { F1re_OnOig1tCj, (*pbstrPi)[j+2]-L’0*): > F or a scripting client to receive events, an object m ust su p p o rt the IP ro v i d e - C1 a s sIn fo 2 interface. T his in terface allow s th e client to inquire as to th e o b ject’s default interface identifier, w hich is then used to establish co ntact via IC o n n ec- tio n P o i ntC ontai ner. ATL provides an im plem entation of IP ro v id eC lass- In fo 2 th at is used like so: class ATL_NO_VTABLE CCalcPi : public CComObjectRootEx, FID OK | C m o d ,匕;| A ddT^prfx. | ATL INTERNALS - ^ .TmTr^iY iT ririT m a fyi-i|iv'■ • u-jOgt-i' •、.. v-. a^kv<.v'.、 •, 工‘..a- . v ':.- 八: ;•• Hit publi c ICoon«ct1onPo1ntConta1n«rI«pl• publi c CProxy„ICa1cP1Events, public IProvideClassInfo2I_pl<4CLSID_CalcPi, IDIID—ICalcPiEvents〉, public: BECIN 一 COLMAP(CCal cPi) C0IUNTERFACE^ENTRY(IConnect1onPo1ntCont«<»»€r) COM_INTERFAC^.ENTRY (IProvi de O asslnfo) C0H_INTERFACE-.ENTRY(IProvideaassInfo2) ... END一 C0HJ4APO KC»eCONNKTIOrLPOINTJUP(CCalcPi〉 CONNECnOK.POIMT^EWTRY(OIIO_rCal cP^ Events) ENO^COMNCCnOHJ*OIMT^UP() T his object is now able to send events that can be handled in a page of HTM L, l i k e s o : 0istribution of first 50 digits in pi: * Handle button click event sub cmdCalcP1_onClick spanPi.innerText » objPiCalculator.CalcPi end sub • HauNfU calculator digit «vwit sub objPIGi.culator.cmDlgiti inu«A• Uigiu HELLO. ATL select case digit case 0: spanO.innerText = spanO.innerText + 1 case 1: spanl.innerText = spanl.innerText + 1 ... end select spanTotal.innerText : spanTotal.彳nnerText + 1 end sub T he sam ple H TM L page handles these events to provide th e first 50 digits of pi and th eir distribution, as show n in F igure 1.11. For m ore inform ation about ATL’s support for connection points, see C hap­ t e r 8 . Using a Window B ecause this is M icrosoft W indow s w e’re developing for, som etim es it’s handy to be able to put up a w indow or a dialog box. For exam ple, the M essageB ox call w e m ade earlier yielded a som ew hat boring advertisem ent, as show n in Figure 1.12. N orm ally, putting up a custom dialog is kind of a pain. F or the average C+ + p r o ­ gram m er, it either involves lots of procedural code, w hich w e don’t like, or it in­ volves building a bunch of forw arding code to m ap W indow s m essages to m em ber l) \ A 1 IBookVsirVChaptei 01 \PiSvi\calcpi3 htm Miciosolt Intern 1 1 D:\ATLBook\sfcAChaptef 01 \PiSvr\calcpi3 htm Im m B E B m r n 3.14159265358979323846264338327950288419716939937510 Distribution of first 50 digits in pi. D i g i t 0 r 2 3 4 5 6 7 8 9 T o t a l F r e q u e n c y 2 5 5 8 4 5 4 4 5 8 5 0 z i A Figure 1.11. Pi to 50 digits sa^jadojd p § 一 s: 安 5» 0 T1 V lBpads-80 73 5 > 1. snBE 1 1 ^^ ................. - - - • ■ ' • ----------- mm 谈 參 、 .. .• . d R S I aao M m . i i \< I 3 ' » I I ___ 1SAPVI - i 」 c l l s d o l Q. pss paxo «J J< ssep xoq go^lp ^ t> o .s t: asul Co 「 T- £ 3 o) iI a 1 _ I S ^« E f ljl _lv : i X V xoq ag ct issau! w) u -c oa _ 3 r- . l . ajnw il ulip cc _t 3 s*> ax. *o 3 i s o x d: 3 p 3 SIVN cc UJ i N I 1 1 V HELLO, ATL functions (a dialog is an object, after all). L ike M FC , A TL has a g reat deal of func­ tionality for building w indow s and dialogs. T o add a new dialog, th e A TL O bject W iz­ ard provides the D ialog object as part of the M iscellaneous category, show n in F igure 1.13. T he D ialog-specific p art of th e A TL O bject W izard (F igure 1.14) is m uch sim pler than other p arts and only allow s you to enter C + + nam e inform ation, be­ cause a dialog is a W in32 object, n o t a C O M object. T he generated code creates a class th at derives from CA xDi a l o glm pl and uses a new dialog box tem plate, also provided by the w izard. T he derived class routes m essages to handlers using the M SG_M AP m acros, as show n here: class CAdvert : public CAxOialogImpl { public: CAdvert() {} -CAdvertC) {} enum { 100 - ID0.A0VERT }; BECIN_MSC.MAP(CAdvert) M ESSACEJUNOLER(W H.INrn)IALOC • O nlni tO ialog) COHMAND.IO.HAWOLERCIOOK. OnOK) COMMANO.IO_HANOLER(IOCANCEL, OnCancel) ENOJIS CComBSTR m_bstrClient; If you’d like to handle another m essage, you can add the 叩 propriate entries to the m essage m ap and add the handler m em ber functions by hand. If you pre­ fer, you can also run the M essage H andler w izard, accessible by right-clicking on the nam e of the CW i ndowlmpl or CDi alo g lm p l-b ased class in the C lassV iew and choosing A dd W indow s M essage H andler. T he M essage H andler dialog is show n in F igure 1.15. For m ore inform ation on ATL,s extensive support for w indow ing, including building standalone W indow s applications, see C hapter 9. WM CAPTURECHAN6ED WM CHAR WM CHARTOITEM WH CLOSE WM WM.COI WM COI COMPAREITEM NTEXTMENU FVDATA WH CREATE WH_DELETEITEM WM DESTR( WM HSl WH KEN WM WM WM WM WM WM WM WM WM I0Y ITEMWM_DRAW11 SCROLL YD OWN WM KEVUP K1LLF0CUS _LBUTT0ND8LCU WM LBUTT0ND0WN WM LBUTTONUP WM MEASUREITEM WM_M0USEM0VE MOUSEWHEEL MOVE WM PAINT j d ■ t o k |mmm 1 1 一 I00K nmm _ m------— ■ ~0 1 V rw TO CW%u — _ _ _ I D U o g m mmmmmmmrnm/mfM Flgur# 1.15. Adding a windows message handler HELLO. ATL Implementing Component Categories If yo u ’ve been follow ing along, o r if yo u ’ve tried to run y our C O M o b jects inside of Internet E xplorer before, you m ay have noticed the really annoying dialog show n in F igure 1 • 16. T his is In tern et E x p lo rer’s w ay o f telling y o u th at th e C a lc P i o b ject h as n o t prom ised to be safe in the paranoid w orld of th e Internet. S ince calculating pi can never really do any harm , w e'd like IE to stop bothering our users (o r at least us) w ith this accusatory dialog. T o let IE know w e prom ise to behave, w e have to list our class in the registry as im plem enting the proper com ponent categories. A com ponent category is a w ay of listing a class as behaving in a certain w ay. F o r ex ­ am ple, the E m bedded com ponent category says that an object of the class can be em bedded using the O LE protocols. T o prom ise to do nothing bad, n o m atter th e initialization d ata o r th e scrip t th at is run against u s (th e w o rse w e could do is crash a single p ro cess), o u r C al cP i class needs to im plem ent the Safe F or Scripting and Safe F or Initialization com ponent categories. W e can do that using the A TL CATECORY_M AP and the appropriate com ­ ponent category unique ID s (C A TID s): class ATL_NO 一 VTABLE CCalcPi : ... { public: BECIfCCATEGORY_HAP(CCalcPi) IMPLEMENTED一 CATEGORY(CATID»SafeForScripting) IMPLEMEMTED„CATEGORY(CATID_SafeForInitializing) END_CATEOORY_MAP() Figure 1.16. Internet E xplorer S ecurity A lert dialog ATL INTERNALS N ot only are com ponent categories good for m aking prom ises to Internet E xplorer, they’re also good for publishing classes to arbitrary clients, as w ell as requiring functionality of clients before they b o th er to create instances. F or m ore inform ation on A TL’s support for com ponent categories, see C hapter 4. Adding a User Interface CO M controls are objects that provide their ow n user interface (U I), w hich is closely integrated w ith that of their clients. A TL provides extensive support for COM controls via the C C om C ontrol base class as w ell as various other base IX xxIm pl classes. T hese base classes handle m ost of the details of being a basic control (although th ere’s plenty of room for advanced features, as show n in C hap­ ter 10). H ad you ch o sen F ull C ontrol o r L ite C ontrol from the C ontrols section of th e ATL O bject W izard w hen generating the C al cP i class, you could have provided the UI m erely by im plem enting the O nD raw function: HRESULT CCalcPi : :OnOraw(ATL_DRAWINFO& di) { CComBSTR bstrPi; if( SUCCEEDED(this->CalcPi(AbstrPi)) ) { USES^CONVERSION; DrawText(di.hdcDraw, 0LE2CT(bstrPi), -1, (RECT*)di.prcBounds, DT_SINCLELINE 丨 DT_CENTER | DT^VCENTER); return S_0K; } T he w izard w ould also have generated a sam ple HTM L page, w hich I've aug­ m ented to tak e up the entire brow ser w indow and to set the initial num ber of digits t o 5 0 : ATL 3.0 test page for object CalcPi HELLO, ATL D isplaying this sam ple page in Internet E xplorer yields a view of a control (Fig­ u r e 1 . 1 7 ) . F or m ore inform ation about building controls in A TL, see C hapter 10. Hosting a Control If you'd like to h o st a control, you can do so w ith A TL’s co n tro l hosting sup p o rt. F or exam ple, the Ax in CA xDi alo g lm p l stands for A ctiveX control and indicates th at th e dialog is ab le to h o st co n tro ls. T o h o st a co n tro l in a dialog, right-click o n th e di­ alog box reso u rce an d ch oose In sert A ctiveX C ontrol. T his w ill p ro d u ce a dialog list­ ing the controls installed on your system , as show n in Figure 1.18. O nce you’ve inserted the control, you can right-click on it and set its properties, as show n in F igure 1.19. A lso by right-clicking, you can choose to handle a control’s events, as show n in Figure 1.20. W hen th e dialog box is show n, the control w ill b e created and initialized based on the properties set at developm ent tim e. A n exam ple of a dialog box hosting a control is show n in F igure 1.21. A TL provides su p p o rt for hosting A ctiveX controls n o t only in dialogs, b u t also in other w indow s, in controls that have a U I declared as a dialog box resource (called com posite controls), and in controls th at have a U I declared as an H TM L re­ source (called H TM L controls). For m ore inform ation about control containm ent, see C hapter 11. 3 14159265358979323846264338327950288419716939937510 F igure 1.17. T he C alcPi control hosted in Internet E xplorer 30 ATL INTERNALS Figure 1.18. In sert A ctiveX C ontrol dialog Figure 1.19. C ontrol P roperties dialog HELLO. ATL 31 C^cPi sponsoied by CalcPi Client F igure 1.21. A dialog hosting a CO M control 二 : 二 二 ....................... .......................... ................................ .....................................................................................• - F igure 1.20. C hoosing w hich control events to handle c a l c h i 「 厂 AT L INTERNALS Summary T his chapter has been a w hirlw ind tour through som e of the functionality of ATL that the w izards expose, as w ell as som e of the basic interface im plem entations of ATL. E ven w ith the w izards, it should be clear th at A TL is no substitute for solid COM know ledge. You still have to know how to design and im plem ent your inter­ faces. A s you’ll see throughout the rest of this book, you still have to know about interface pointers, reference counting, runtim e type discovery, threading, persis­ tence … the list goes on. ATL can help, but you still have to know CO M . It. sh o u ld also be clear th at th e w izard is n o t a su b stitu te fo r in tim ate A TL k n o w l­ edge. F or every tidbit of A TL inform ation I’ve show n in this chapter, there are ten m ore salient details, extensions, and pitfalls. A nd although the w izard saves you typing, it can’t do everything. It can’t m ake sure your design and im plem entation goals are m et: T hat’s up to you. CHAPTER ATL Smart Types Strings,BSTRs, VARIANTS, and Interface Pointers C O M has a num ber of data types beyond the num eric types available in the C lan­ guage. T hree such d ata types are tex t strings (especially in th e form o f a BSTR), the VARIANT data type, and interface pointers. A TL provides useful classes that en cap ­ su late each of these d ata ty p es and th eir special idiosyncrasies. Strings com e in 汪 num ber of different character sets. CO M com ponents often need to use m ultiple character sets and occasionally need to convert from one set to another. A TL provides a num ber of string conversion m acros (w hich it inherited from the M FC source code base), for exam ple, A2W, 0LE2C T, and others, th at con­ v ert from one ch aracter set to another, if necessary, and do nothing w hen they are not needed. T he CComBSTR class is a sm art string class. T his class properly allocates, copies, and frees a string according to the BSTR string sem antics. CComBSTR in­ stan ces can be used in m ost, b u t n o t all, of th e places you w ould use a BSTR. T he C C om V ariant class is a sm art VARIANT class. T he class im plem ents the special initialization, copy, and destruction sem antics of the CO M VARIANT data type. C C om V ariant instances can be used in m ost, but not all, of the places you w ould use a VARIANT. T he C C om Ptr and C C om Q IPtr classes are sm art pointer classes. S m art pointer classes are definitions of objects th at act like a p o in ter, specifically, a p o in ter w ith ex tra sem antics. O ne exam ple, in the case of sm art interface pointers, is th at the destructor can release the interface pointer any tim e the sm art pointer goes ou t of scope, including unusual situations like exception handling. String Data Types, Conversion Macros, and Helper Functions A Review of Text Data Types T he text data type is rather a pain to deal w ith in C + + program m ing. T he m ain problem is th at th ere isn’t one, single text d ata type. T here are too m any of them . I’m using the term text data type here in the general sense of an array of charac­ ters. O ften, different operating system s and program m ing languages introduce ATL INTERNALS additional sem antics on an array of characters (for exam ple, NUL character term i­ nation o r a length prefix) befo re they co n sid er an array of ch aracters a tex t string. W hen you select a tex t d ata type, th ere are a n u m b er o f decisions to m ake. F irst, there is the issue of w hat type of characters constitute the array. Som e operating system s require you to use A N SI characters w hen you pass a string (for exam ple, a file nam e) to the operating system . Som e operating system s prefer you to use U ni­ code characters but w ill accept A N SI characters. O ther operating system s require you to use EB C D IC characters. T here are stranger character sets in use as w ell, such as the M ulti/D ouble B yte C haracter Sets (M B C S/D B C S), the details of w hich this book largely d o esn ’t discuss. Second, th ere is th e question of w hat ch aracter set you use w hen w riting a pro­ gram . T here is no requirem ent that your source code use the sam e character set as the one preferred by the operating system running your program . C learly, it’s m ore convenient w hen both use the sam e character set. N evertheless, a program and the operating system can use different ch aracter sets. Y ou “sim ply” have to convert all text strings going to and com ing from the operating system . T hird, th ere is the issue of determ ining the length of a tex t string. Som e lan­ guages, such as C and C + + , and som e operating system s, such as W indow s 9x/N T and U N IX , u se a term inating NUL ch aracter to delim it th e end of a tex t string. O ther languages, such as M icrosoft’s V isual B asic interpreter, M icrosoft’s Jav a virtual m a­ chine, and P ascal, p refer an explicit length prefix specifying th e num ber of charac­ ters in th e tex t string. Fourth, in practice, a tex t string presents a resource m anagem ent issue. T ext strings typically vary in length. T herefore, for efficient m em ory usage, a tex t string Ls o ften dynam ically allocated. O f co u rse, th is m ean s a tex t strin g m u st b e freed ev en ­ tually. R esource m anagem ent introduces the idea of an ow ner of a tex t string. T he ow ner of the string, and only the ow ner, frees the string and only frees it once. O w nership becom es quite im portant w hen you pass a text string betw een com po­ nents distributed across m ultiple heterogeneous com puters. Tw o CO M objects, how ever, m ay reside on tw o different com puters running t w o different operating system s th at prefer tw o different ch aracter sets for a tex t string. F or exam ple, you can w rite one C O M object in V isual B asic and run it on the W indow s N T operating system . You m ight pass a text string to another CO M object w ritten in C + 十 running on an IBM m ainfram e. C learly, w e need som e standard text data type th at all CO M objects in a heterogeneous environm ent can understand. COM uses the OLECHAR character data type. A COM text string is a NUL- character-term inated array of OLECHAR characters, and a po in ter to su ch a string is an LPO LESTR.1 A s a rule, a tex t string p aram eter to a C O M interface m ethod should 1 Noto that the actual underlying character data type for OLECHAR on one operating system (for example, Windows NT) can be different from the underlying character data type for OLECHAR on a different ATL SMART TYPES be of type LPOLESTR. W hen a m ethod doesn’t change the siring, the param eter should be of type LPCOLESTR, that is, a constant pointer to an array of OLECHAR characters. Frequently, though not alw ays, the OLECHAR type isn't the sam e as the ch arac­ ters you use w hen w riting your code. Som etim es, though not alw ays, the OLECHAR type isn’t the sam e as the characters you m ust provide w hen passing a tex t string to the operating system . T his m eans that, depending on context,som etim es you’ll need to convert a text string from one character set to another and som etim es y o u w o n ’ t . U nfortunately, a change in com piler options (for exam ple, a W indow s N T U ni­ code build or a W indow s C E build) can change this context, resulting in code that previously didn’t need to convert a string now needing conversion and vice versa. Y ou don’t w ant to rew rite all string m anipulation code each tim e you change a com ­ piler option. T herefore, A TL provides a num ber of string conversion m acros that convert a tex t string from one ch aracter set to an o th er and are sensitive to the con­ text in which you invoke the conversion. Windows Character Data Types N ow , let’s focus specifically on the W indow s platform . W indow s-based CO M com ­ ponents typically use a m ix of four tex t d ata types. ■ U n ico d e: A specification for rep resen tin g a ch aracter as a uw ide-character,M 16- bit m ultilingual character code. The W indow s N T operating system uses the U nicode character set internally. All characters used in m odem com puting w orldw ide, including technical sym bols and special publishing characters, can be represented uniquely in U nicode. T he fixed character size sim plifies pro­ gram m ing using international ch aracter sets. In C /C + + , you rep resen t a w ide- character string as a w ch ar_ t array; a pointer to such a string is a w ch ar_ t* p o i n t e r . ■ MBCS/DBCS: T he M ulti-B yte C haracter Set is a m ixed-w idth character sot, in w hich som e characters consist of m ore than one byte. T he W indow s 9x op­ erating system s, in general, use the M B C S to represent characters. T he D B CS (D ouble-B yte C haracter S et) is a specific type of m ultibyte ch aracter set. It in­ cludes som e characters that consist of one byte and som e characters th at con­ sist of tw o bytes to represent the sym bols for one specific locale, such as the Japanese, the C hinese, and the K orean languagos. operating system (for example, OS/390). The COM remoting infrastructure performs any necessary character set conversion during marshaling and unmarshaling. Therefore, a COM component always receives text in its expected OLECHAR format. ATL INTERNALS In C /C + + , you represent an M BCS/D BCS string as an unsigned char array; a pointer to such a string is an u n si gned ch ar* pointer. Som etim es a character is one u n sig n ed ch ar in length. Som etim es it’s m ore than one. T his is loads of fan to deal w ith, especially w hen trying to back u p through 21 s t r i n g . In V isual C + + , M BCS alw ays m eans D BCS. C haracter sets w ider than tw o bytes are n o t supported. ■ A N SI: Y ou can represent all characters used by the E nglish language, as w ell as m any w estern E uropean languages, using only eight bits. V ersions of W in­ dow s supporting such languages use a degenerate case of M BCS, called the M icrosoft W indow s A N SI character set, in w hich no m ultibyte characters are present. T he M icrosoft W indow s A N SI character set, w hich is essentially ISO 8859/x plus additional characters, w as originally based on an AN SI draft s t a n d a r d . T he A N SI character set m aps the letters and num erals in the sam e m anner as A SCII. H ow ever, A N SI does not support control ch aracters and it m aps m any sym bols, including accen ted letters, th at are not m apped in stan d ard A SC II. A ll W indow s fonts are defined in the A N SI character set. T his is also called the SB C S (Single-B yte C haracter Set) for sym m etry. In C /C + + , you rep resen t an A N SI string as a c h a r array; a pointer to such a strin g is a ch a r ☆ pointer. A ch aracter is alw ays o n e c h a rin length. B y default, a char is a signed char in V isual C + + . B ecause M BCS characters are u n sig n ed and A N SI characters are, by default, sig n ed characters, expres­ sions can evaluate differently w hen using A N SI characters as com pared w ith using M B C S characters. ■ TCH A R/JTCH A R: A M icrosoft-specific generic-text d ata type th at you can m ap to a U nicode character, an M B C S character, or an A N SI character using com pile- tim e options. Y ou u se this ch aracter type to w rite generic co d e that can be com ­ piled for any of the three ch aracter sets. T his sim plifies code developm ent for international m arkets. T he C runtim e library defines the __TCHAR type, and the W indow s operating system defines the TCHAR type. T hey are synonym ous. N ote: tc h a r . h, a M icrosoft-specific C runtim e library header file, defines the generic- text data type _TCHAR, ANSI C /C + + com piler com pliance requires im plem entor- defined names to be underscore prefixed. When you do not define the — STDC p rep ro cesso r sym bol (the default setting in V isual C + + ), you indicate you don’t require A N SI com pliance. In this case, the tc h a r .h header file also defines the sym bol TCHAR as another alias for the generic-text data type if it isn’t already defined, w in n t.h , a M icrosoft-specific W in32 operating system head er file, defines th e gerveric-text d ataty p e TCHAR. T his header file is operating system specific, so the sym bol nam es don't need to have the underscore prefix. ATL SMART TYPES Win32 APIs and Strings E ach W in32 A PI th at requires a string has tw o versions: one th at requires a U nicode argum ent and another that requires an M BCS argum ent. O n a non-M B C S-enabled version of W indow s, the M BCS version of an API expects an A N SI argum ent For exam ple, the SetW i ndow T ext A PI doesn’t really exist. T here are actually tw o functions: SetW i ndow TextW , w hich expects a U nicode string argum ent, and S e t- Wi ndow T extA , w hich expects an M BC S/A N SI string argum ent. T he W indow s N T operating system internally uses only U nicode strings. T here­ fore, w hen you call SetW i ndow T extA on W indow s NT, the function translates the specified string to U nicode, then calls SetW i ndow TextW . T he W indow s 9x operating system s do not support U nicode. T he SetW i ndow T extA function on the W indow s 9x operating system s does the w ork, w hereas SetW i ndow TextW returns a n e r r o r . T his gives you a difficult choice. Y ou can w rite a perform ance-optim ized com ­ ponent using U nicode character strings that runs on W indow s N T bu t not on W in­ dow s 9x. You can w rite a m ore general com ponent using M BCS/A N SI character strings th at runs on both operating system s, but n o t optim ally on W indow s N T. A l­ ternatively, you can hedge your b ets by w riting source code in such a w ay th at you can decide at com pile tim e w hat character set to support. A little coding discipline and som e p rep ro cesso r m agic lets you code as if there w ere a single A PI called SetW i ndow T ext that expects a TCHAR string argum ent. Y ou specify at com pile tim e w hich kind of com ponent you w ish to build. F or ex­ am ple, you w rite code that calls SetW i ndow T ext and specifies a TCHAR buffer. W hen com piling a com ponent as U nicode, you actually call SetW i ndow TextW and the argum ent is a w ch ar_ t buffer. W hen com piling an M BCS/A N SI com ponent, you actually call SetW i ndow T extA and the argum ent is a c h a r buffer. W hen you w rite a W indow s-based CO M com ponent, you should typically use th e TCHAR ch aracter type to rep resen t characters used by the com ponent internally. A dditionally, use it fo r all ch aracters u sed in in teractio n s w ith th e o p eratin g system . Sim ilarly, you should use the TEX T o r — TEX T m acro to surround every literal char­ acter or string, tc h a r. h defines the functionally equivalent m acros 一 T , _ T , a n d _TEX T, w hich all com pile a ch aracter or strin g literal as a generic-text ch aracter or literal, w i n n t. h also defines the functionally equivalent m acros TEX T and _ TEXT, w hich both do exactly the sam e thing as __T, _ T, and _TEX T. T here’s nothing like five w ays to do exactly the sam e thing. T he exam ples in this chapter use _ TEXT b ecau se it’s defined in w i n n t. h. I actually p refer 一 T because it’s less clu tter in m y source code. A n operating-system -agnostic coding approach w ould favor including tc h a r • h and using the _TCHAR generic-text data type because th at’s som ew hat less tied to ATL INTERNALS the W indow s operating system s. H ow ever, w e’re discussing building com ponents w ith tex t handling optim ized at com pile tim e for specific versions of the W indow s operating system s. T his argues w e should use TCHAR, th e type defined in w i n n t. h. Plus, TCHAR isn’t as jarrin g to th e ey es as 一 TCHAR. It’s less to type. M ost code al­ ready im plicitly includes the w i n n t. h. h ead er file, an d you m u st explicitly include tc h a r.h . T here are all sorts of good reasons to prefer TCHAR, so the exam ples in this b ook use TCHAR as the generic-text d ata type. T his allow s you to com pile specialized versions of the com ponent for different m arkets or for perform ance reasons. T hese types and m acros are defined in the w i n n t. h h ead er file. Y ou also m ust use a different set of string runtim e library functions w hen m anipulating strings of TCHAR characters. T he fam iliar functions s tr l e n ,s t r c p y , and so on only operate on ch ar characters. T he less fam ilar functions w cslen , w cscpy, and so on w ork on w ch ar^t characters. M oreover, the totally strange functions _m bslen, 一 m bscpy,and so on w ork on m ultibyte characters. B ecause TCHAR characters are som etim es w char_t and som etim es char-holding ANSI characters and som etim es char-holding (nom inally u n sig n ed ) m ultibyte charac­ ters, you need an equivalent set of runtim e library functions th at w ork w ith TCHAR characters. T he tc h a r • h h ead er file defines a num ber of useful generic-text m appings for string handling functions. T hese functions ex p ect TCHAR param eters, so ail their function nam es use the _ tc s (the _ t character set) prefix. F or exam ple, _ tcs1 e n is equivalent to the C runtim e library s tr l en function. T he _ tc s le n function ex­ pects TCHAR characters, w hereas the s trle n function expects ch ar characters. Controlling Generic-Text Mapping Using the Preprocessor T w o preprocessor sym bols and tw o m acros control the m apping of the TCHAR data type to the underlying ch aracter type used by th e application. ■ UN ICOD E/_UNICO DE: T he header files for the W indow s operating system A PIs use the UNICODE preprocessor sym bol. T he C /C + + runtim e library h eader files use the —UNICODE preprocessor sym bol. Typically, you’ll either define both sym bols or define neither of them . W hen you com pile w ith the sym bol _U N I- CODE defined, tc h a r • h m aps all TCHAR ch aracters to w c h a r_ t characters. T he 一 T ,_ T , a n d 一 TEX T m acro s prefix each ch aracter o r string literal w ith a capital L (creating a U nicode ch aracter or literal, respectively). W hen you com pile w ith the sym bol UNICODE defined, wi n n t. h m aps all TCHAR characters to w ch ar_ t characters. T he TEX T and _ TEXT m acros prefix each character or string literal w ith a cap ital L (creatin g a U nicode ch aracter o r literal, respectively). ATL SMART TYPES ■ _ _ M B C S : W hen you com pile w ith the sym bol _M BCS defined, all TCHAR characters m ap to ch ar characters and the preprocessor rem oves all the _T and 一T E X T m acro variations, leaving the ch aracter o r literal unchanged (creating an M B C S ch aracter or literal, respectively). ■ W hen you com pile w ith neither sym bol defined, all TCHAR characters m ap to ch ar characters and the preprocessor rem oves all the _T and _ TEXT m acro variations, leaving the character or literal unchanged (creating an A N SI char­ acter o r literal, resp ectiv ely ). Y ou w rite generic-text-^om patible code by using th e generic-text d ata types and functions. Som e generic-text code th at reverses the order of the characters in a string and th en ap p en d s a string literal as follow s: TCHAR *reversedString, *sourceString, *complGteString; reversedString « _tcsrev (sourceString); completeString = _tcscat (reversedString, TEXT("suffix")); W hen you com pile the code w ithout defining any preprocessor sym bols, the preprocessor produces this output: char ^reversedString, *source5tringf *comp!eteString; reversedString « _strrev (sourceString); completeString = strcat (reversedString, "suffix"); W hen you com pile the code after defining the —UNICODE preprocessor sym bol, the preprocessor produces this output : _i»char.t * reversedString, * sourceStri ng, *co(npleteString; reversedString * _wcsrev (sourceString); completeString * wcscat (reversedString, L"suffix"); W hen you com pile the code after defining the __MBCS p rep ro cesso r sym bol, the preprocessor produces this output : char *reversedString, *sourceString, *completeString; reversedString * jibsrev (sourceString); completeString * jibscat (reversedString, "suffix"); ATL INTERNALS COM Character Data Types CO M uses tw o character types. ■ OLECHAR: T he character type used by CO M on the operating system for w hich you com pile your source code. For W in32 operating system s, this is the w ch ar_ t character type. Note: Actually, you can change the Win32 OLECHAR data type from the d e f a u l t w c h a r _ t (which is what COM uses internally) to char by defining the preprocessor symbol 0LE2ANSI. This lets you pretend that COM uses ANSI. MFC used this feature at one time. Don't give in to the dark side, Luke. Using this “convenience” feature requires that you then link your com ponents w ith the 0LE2A N SI thunklng library. T his library con­ verts all AN SI string param eters back to w char_t on every API and COM interface method call. Similarly, it converts output strings from w char_t to char. This adds need­ less overhead in many cases. MFC’s performance improved roughly 10 percent when Microsoft rewrote it without using 0LE2ANSI and used the string conversion macros where necessary. F or W in 16 operating system s, this is the c h a r ch aracter type. F or the M ac O S, this is th e c h a r ch aracter type. F or th e S olaris O S, this is the w c h a r_ t ch ar­ acter type. F or the as yet unknow n operating system , this is w ho know s w hat. L et’s ju st pretend there is an ab stract d ata type called OLECHAR. C O M uses it. D on’t rely on it m apping to any specific underlying d ata type. ■ BSTR: A specialized string type used by som e CO M com ponents. A BSTR is a length-prefixed array of OLECHAR ch aracters w ith num erous special sem antics. N ow let's com plicate things a bit. Y ou w an t to w rite code for w hich you can se­ lect, at com pile tim e,the type of ch aracters it uses. T herefore, you’re m anipulating strictly TCHAR strin g s internally. Y ou also w an t to call a C O M m eth o d an d p ass it th e sam e strings. Y ou’ll have to pass the m ethod either an OLECHAR string o r a BSTR string, depending on its signature. T he strings used by your com ponent m ay or m ay not be in th e co rrect ch aracter form at, depending on your com pilation options. T his is a jo b for superm acro. ATL String Conversion Macros ATL provides a num ber of string conversion m acros th at convert, w hen necessary, betw een the various character types described previously. T he m acros perform no conversion, and, in fact, do nothing, w hen the com pilation options cause the source and destination ch aracter ty p es to b e identical. T he m acro nam es use a num ber of abbreviations for the various character d a t a t y p e s : ATL SMART TYPES ■ T represents a pointer to the W in32 TCHAR character type— a LPTSTR p a r a m e t e r . ■ W represents a pointer to the U nicode w ch ar_ t character type 一a L P W S T R p a r a m e t e r . ■ A represents a pointer to the M BCS/ANSI ch ar character type 一a L P S T R p a r a m e t e r . ■ O L E represents a pointer to the CO M OLECHAR c h a r a c te r type— a LPOLESTR p a r a m e t e r . _ C represents the C/C 十 + c o n s t m o d ifie r. All m acro nam es use the form u2”; for exam ple, the A2W m acro converts a LPSTR to a LPW STR. W hen th ere is a C in the m acro nam e, add a c o n st m odification to the follow ing abbreviation; for exam ple, the T2C 0LE m acro converts a LPTSTR to a LPCOLESTR. T he actual m acro behavior depends on w hich preprocessor sym bols you define (T able 2.1). T able 2.2 lists th e A TL string conversion m acros. All m acros accept one argum ent, w hich is a pointer to a string in the source ch aracter set. E ach m acro acts like a function call th at retu rn s a p o in ter to a string Table 2.1. C haracter Set Preprocessor Sym bols Preprocessor symbol defined T becomes OLE becom es N o n e A W 一 UNICODEW w 0 L E 2 A N S I AA .UNICODE and 0LE2A NSI W A Table 2.2. ATL String C onversion M acros A 2 B S T R A2W 0LE2CW T2C0LE W2CA A2C0LE A2WBSTR 0 L E 2 T T 2 C W W 2 C 0 L E A 2 C T 0LE2A 0LE2W T20LE W 2 C T A2CW 0LE2BSTR T 2 A T 2 W W 2 0 L E A 2 0 L E 0LE2CA T2BSTR W 2 A W 2 T A2T 0LE2CT T2CA W2BSTR ATL INTERNALS in the destination ch aracter set. W hen th e source and destination ch aracter sets are the sam e, the m acro sim ply returns the specified argum ent as th e destination. W hen the source and destination character sets are different and the destina­ tion type is not BSTR, th e m acro allo cates the d estin atio n string using th e _ a l 1 o c a runtim e library function. T he _ a llo c a function allocates m em ory on the stack (th at is, it grow s the stack pointer). T he conversion m acro s allocate th e destination string on the stack so th at w hen your function (the one using the conversion m acro) returns, the m em ory is autom atically reclaim ed. H ow ever, it’s im portant to r e a l i z e w hat is happening w ith the m em ory so you don’t use the conversion m acros w ithin a loop. R epeated invocations of these conversion m acros w ithin a loop grow the stack larger an d larger. E ventually, you w ill ru n out of stack space. T he conversion m acros use local variables. Y ou m ust allocate these variables by specifying the U SES 一 CONVERSION m acro once at the beginning of any function th at u ses a string conversion m acro. T he follow ing code converts a TCHAR string to an OLECHAR string and calls a CO M m ethod th at expects a constant pointer to an OLECHAR string. Y ou do n ’t n eed to free the OLECHAR string becau se it w ill be freed autom atically w hen the function returns. STDMETHODIMP put^Name (/* [in] */ const OLECHAR* pName); void SetName (LPTSTR lpsz) { USES.CONVERSION; _ 參 《 pObj->put_Name (T2C0LE(1psz)); } W hen the source and destination character sets are different and the destina-y tion type is BSTR, th e m acro allocates the destination string using th e SysA I 1 o c - S trin g or SysA I ^ o c S tri ngL en function. Y ou m ust explicitly free this BSTR using S y sF reeS trin g . T he follow ing code converts a TCHAR string to a BSTR, calls a C O M m ethod that expects a BSTR string, and then frees the BSTR. void SetName (LPTSTR lpsz) { USES_CONVERSION; BSTR bstr « T2BSTR(lpsz); pObj->put_Name (bstr); ::SysFreeString (bstr); ATL SMART TYPES ATL String Helper Functions Som etim es you w ant to copy a string of OLECHAR characters. Y ou also happen to know th at OLECHAR characters are w ide characters on the W in32 operating system . W hen w riting a W in32 version of your com ponent, you m ight call the W in32 oper­ ating system function IstrcpyW , w hich copies w ide characters. U nfortunately, W indow s NT, w hich supports U nicode, im plem ents IstrcpyW , but W indow s 95 does not. A com ponent that uses the IstrcpyW API doesn’t w ork correctly on W indow s 95. Instead of 1 strcpyW , use the ATL string helper function o cscp y to copy an OLECHAR character string. It w orks properly on both W indow s N T and W indow s 95. T here is an ATL string helper function o cslen that returns the length of an OLECHAR string. T his is nice for sym m etry, though the Istrle n W function it re­ places does w ork on both operating system s. OLECHAR* ocscpy(LPOLESTR dest, LPCOLESTR src); size»t ocslen(LPCOLESTR s); Sim ilarly, the W in32 C harN extW operating system function doesn’t w ork on W indow s 95, so ATL provides a C harN extO string helper function that increm ents an OLECHAR* by one character and returns the next character pointer. It w ill not increm ent the pointer beyond a NUL term ination character. LPOLESTR CharNextO(LPCOLESTR Ip); The CComBSTR Smart BSTR Class A Review of the COM String Data Type— BSTR CO M is a language-neutral, hardw are-architecture-neutxal m odel. T herefore, it needs a language-neutral, hardw are-architecture-neutral text data type. U nfortu­ nately, th ere is n o single tex t d ata type th at is universally available on all h ard w are and usable by all languages. Som e platform s use the A SC II/A N SI character set, w hich consists of 8-bit characters. S om e use the EB C D IC ch aracter set, w hich con­ s i s t s of a different 8-bit character set. Som e use the M B C S (M ulti-B yte C haracter S et), w hich consists of interm ingled 8-bit and 16-bit characters. S om e use th e U ni­ code character set, w hich consists of 16-bit characters. T here are other, less fre­ quently used, character sets in well. CO M defines a generic text ype, OLECHAR, w hich represents the text d 抑 u sed by C O M o n a specific platform . O n m o st platform s, including all 32-bit W indow s platform s, the OLECHAR data type is a ty p e d e f for the w ch ar 一 t d ata type. T h at is, ATL INTERNALS on m ost platform s, the COM text data type is equivalent to the C /C + + w ide- character d ata type, w hich contains U nicode characters. O n som e platform s, such as the 16-bit W indow s operating system and the M acintosh O S, OLECHAR is a ty p e d e f for th e stan d ard C c h a r d ata type, w hich co n tain s A N SI ch aracters. G enerally, y ou’ll w an t to define all strin g p aram eters used in a C O M interface asO LEC H A R* argum ents. CO M also defines an additional tex t data type called BSTR. A BSTR is a length- prefixed string of OLECHAR characters. M ost interpretive environm ents prefer length-prefixed strings for perform ance reasons. F or exam ple, a length-prefixed string requires no tim e-consum ing scanning for a NUL character term inator to d e­ term ine the length of a string. A ctually, the N U L -character-term inated string is a language-specific concept originally unique to th e C /C + + language. T he M icrosoft V isual B asic in terp reter, th e M icrosoft Jav a virtual m achine, an d m ost scripting lan­ guages, such as V B Script and JS cript, internally rep resen t a string as a BSTR. T herefore, you’ll find th at w hen you p ass o r receive a string to o r from a m eth o d param eter to an interface defined by a C /C + + com ponent, you’ll often use the OLECHAR* d ata type. H ow ever, if you need to use an interface defined by another language, frequently string param eters w ill be the BSTR data type. T he BSTR data type has a num ber of poorly docum ented sem antics, w hich m akes using BSTRs a te­ dious and error-prone task for C + + developers. A BSTR h as the follow ing attributes: ■ A BSTR is a p o in ter to a length-prefixed array of OLECHAR characters. ■ A BSTR is a p o in ter d ata type. It points at the first ch aracter in th e array. T he length prefix is sto red as an integer im m ediately preceding th e first ch aracter m t h e a r r a y . ■ T he array of ch aracters is NUL ch aracter term inated. _ T he length prefix is in bytes, n o t ch aracters, and d o es n o t include th e term inat­ ing NUL character. _ T he array of characters m ay contain em bedded NUL characters. _ It m ust be allocated and freed using the SysA I 1 o cS tri ng and S ysF ree S tri ng fam ily of functions. ■ A NULL BSTR pointer im plies an em pty string. ■ A BSTR is not reference counted; therefore, tw o references to the sam e string content m ust refer to separate BSTRs. In other w ords, copying a BSTR im plies m aking a duplicate string, not sim ply copying the pointer. W ith all th ese special sem antics, it w ould be useful to encap su late th ese details in a reusable class. A TL provides such a class: CCom BSTR. ATL SMART TYPES The CComBSTR Class T he CComBSTR class is an A T L utility class th at is a useful encapsulation for the CO M string data type, BSTR. T he a tlb a s e .h file contains the definition of the CComBSTR class. T he only state m aintained by the class is a single public m em ber variable, m _ str, of type BSTR. ;/////////////////////////////////////////////////////////// II CComBSTR class CComBSTR { public: BSTR m.str; Constructors and Destructor T here are eight co n stru cto rs available for CCom BSTR objects. T he default construc­ to r sim ply initializes th e m _ str variable to NULL, w hich is equivalent to a BSTR th at represents an em pty string. T he destructor destroys any BSTR contained in the n u str variable by calling S y sF reeS tri ng. T he S y sF reeS tri ng function explic­ itly docum ents th at the function sim ply returns w hen the input param eter is NULL so the destructor can run on an em pty object w ithout problem . CComBSTR() { m_str * NULL; > ^•CComBSTRO { ::SysFreeString(m_str) ; } Y ou w ill see, later in this section, num erous convenience m ethods provided by the CCom BSTR class. H ow ever, Fd argue th at one of th e m ost com pelling reaso n s for using th e class is so th e d estru cto r frees th e in tern al BSTR at the appropriate tim e, so you don’t have to free a BSTR explicitly. T his is exceptionally convenient during tim es like stack fram e unw inding, w hen locating an exception handler. P robably the m ast frequently used constructor initializes a CComBSTR object from a pointer to a N U L character-term inated array of OLECHAR characters, or, as it’s m ore com m only know n, a LPCOLESTR. CComBSTRCLPCULESTR pSrc) { iiustr « : :SysAllocString(pSrc);} ATL INTERNALS You invoke the preceding constructor w hen you w rite code such as the f o l l o w i n g : CComBSTR strl (OLESTR ("This is a string of OLECHARs")); T he previous co n stru cto r copies ch aracters until it finds th e end-of-string N U L character term inator. W hen you w ant som e lesser num ber of characters copied, su ch as th e prefix to a string, o r w hen you w an t to copy from a strin g containing em ­ bedded N U L characters, you m ust explicitly specify the num ber of characters to copy. In this case, u se th e follow ing co n stru cto r CCo«BSTR(int nSize, LPCOLESTR sz) { m 一 str « ::SysAllocStringLen (sz. nSize); } This constructor creates a BSTR w ith room for the num ber of characters specified by n S i ze, copies the specified num ber erf characters, including any em ­ bedded N U L characters, from sz, then appends a term inating N U L character. W hen sz is NULL,S y sA H o cS tri ngL en skips the copy step, w hich creates an uninitial­ ized BSTR of the specified size. You invoke the preceding constructor w hen you w rite code such as the follow ing: CCowBSTR Str2 (16, OLESTR ("This is a string of OLECHARs")): // str2 contains "This is a string•’ CComBSTR str3 (64, (LPCOLESTR) NULL); II Allocates an uninitialized BSTR with room for 64 characters CComBSTR str4 (64); // Allocates an uninitialized BSTR with room for 64 characters T he CCom BSTR class prov id es a special co n stru cto r fo r th e s t r 3 exam ple in th e pre­ ceding code, w hich doesn't require you to provide the NULL argum ent T he preced­ ing s tr4 exam ple show s its usage. H ere’s the constructor CCom8STR(int nSize) { nustr ■ ::SysAllocStringLen(NULL, nSize); > O ne odd sem an tic featu re o f a BSTR is th at a NULL p o in ter is a valid value fo r an em pty BSTR string. V isual B asic, fo r exam ple, considers a NULL BSTR to be equiva­ lent to a p o in ter to an em pty string, th at is, a strin g o f zero length w here th e first ch aracter is th e term inating N U L character. O r to p u t it sym bolically, V isual B asic considers IF " ” = p, w here p is a BSTR set to NULL, to be true. ATL SMART TYPES U nfortunately, not all the A PIs dealing w ith BSTRs recognize this fact. Spe­ cifically, the docum entation for the S y sS tri ngL en function explicitly specifies th at the argum ent m ust be non-N U LL. T his m eans to obtain the length of a BSTR cor­ rectly (according to the docum entation), you need to w rite code such as this: UINT length = bstrlnput ? SysStringLen (bstrlnput) : 0 ; A fter you have a BSTR encapsulated in a CCom BSTR object, you can sim ply call the L ength m em ber function w hich does the sam e thing: unsigned int Length() const { return (m一 str *• NULL) ? 0 : SysStringLen(m_str); } Testing under W indow s N T 4.0 Service Pack 3 show s that S y sS tri ngLen doesn’t really care w hether its argum ent is NULL. W hen its argum ent is non- NULL, S y sS tri ngLen returns the proper length. W hen its argum ent is NULL, S y sS t ri ngL en returns zero. H ow ever, this m ay n o t hold true under older versions of W indow s N T, or on other operating system s that support CO M (Solaris, O S/390, etc.). S y sS t ri ngL en m ay have been im plem ented according to the docum entation on other operating system s. Y ou can also use the follow ing copy constructor to create and initialize a CCom BSTR object to be equivalent to an already initialized CCom BSTR o b ject CCo«8STR(const CComBSTRA sre) { m—str * src.CopyO; } In the follow ing code, creating the s tr 5 variable invokes the preceding copy co n stru cto r to initialize th eir resp ectiv e objects: CComBSTR strl (OLESTRC'This is a string of OLECHARs")); CComBSTR str5 - strl ; N ote that the preceding copy constructor called the C opy m ethod on the source CCom BSTR object. T he C opy m ethod m akes a copy of its string and returns the new BSTR. B ecause the C opy m ethod allocates the new BSTR using the length of the existing BSTR and copies the string contents for the specified length, the C opy m ethod properly copies a BSTR that contains em bedded N U L characters. BSTR Copy() const { return ::SysAllocStringLen(m«str, ::SysStringLen(m_str)); } ATL INTERNALS T w o co n stru cto rs initialize a CCom BSTR o b ject from a LPCSTR string. T he single argum ent constructor expects a N U L -term inated LPCSTR string. T he tw o-argum ent constructor perm its you to specify the length of the LPCSTR string. T hese tw o constructors are functionally equivalent to the tw o previously discussed construc­ tors that accept a LPCOLESTR param eter. T he follow ing tw o constructors expect characters and create a BSTR containing the equivalent string in OLECHAR : t e r s . SIC l ANI #i fndef 0LE2ANSI CComBSTR(LPCSTR pSrc) { austr « A2WBSTR(pSrc);} CComBSTR(int nSize. LPCSTR sz) { m_str * A2WBSTR(sz, nSi2 e);} #endif T hese tw o constructors are only present w hen the 0LE2A N SI com piler sym bol is not defined during com pilation. W hen you define the 0LE2A N SI com piler sym bol, the LPCOLESTR data type becom es identical to the LPCSTR data type, so these con­ structors have already been defined. T h e f in a l c o n s t r u c t o r i s a n o d d o n e . I t t a k e s a n a r g u m e n t t h a t i s a G U I D a n d p r ola r g u m c n o f t h educes a string containing the string representation of the G U ID . CCom8STR(REFCUI0 src) {... > T his constructor is quite useful w hen building strings used during com ponent reg­ istration. T here are a num ber of situations w hen you need to w rite the string rep­ resentation of a G U ID to the registry. Som e code that uses this constructor f o l l o w s : // Define a GUID as a binary constant static const GUID CUID_Sample - { 0x8a44ell0, 0xfl34, Oxlldl, { 0x96, Oxbl, OxBA, OxDB, OxAD, OxBA, 0xDBt OxAD > >; // Convert the binary GUID to its string representation CComBSTR str6 (CUID_5ample); // Str6 contains M{8A44EHO-F134-lldl-96Bl-BADBAOBADBAD}M Initialization T he CComBSTR class defines three assignm ent operators. T he first one initializes a CCom BSTR object using a different CCom BSTR o b ject T he second one initializes a CCom BSTR object using a LPCOLESTR pointer. T he third one initializes the object ATL SMART TYPES using a LPCSTR pointer. T he follow ing o p e ra to r= () m ethod initializes one CCom ­ BSTR object from another CCom BSTR object. CComBSTRA operator*(const CCcwnBSTRA src) { if (m-Str U src.m^str) { if (m-Str) ::SysFreeString(m_str); m_str * src.CopyO; } return *this; > N ote that this assignm ent operator uses the C opy m ethod to m ake an exact copy of the specified CComBSTR instance. Y ou invoke this operator w hen you w rite code such as th e follow ing: CComBSTR strl (OLESTRC'This is a string of OLECHARs")); CComBSTR str7 ; str7 = strl; // str7 contains "This is a string of OLECHARs" str7 = str7; // This is a NOP. Assignment operator detects this case The second o p erato r= () m ethod initializes one CComBSTR object from a LPCOLESTR pointer to a N U L character-term inated string. CComBSTR* operator*(LPCOLESTR pSrc) { ::SysFreeString(m_str); m_str - ::SysAllocString(pSrc); return *this; > N ote th at this assignm ent o p erato r uses the SysA I 1 o c S tri ng function to allocate a BSTR copy of the specified LPCOLESTR argum ent You invoke this operator w hen you w rite code such as the follow ing: CComBSTR str8 ; str8 = OLESTR ("This is a string of OLECHARs'*); ATL INTERNALS It’s quite easy to m isuse this assignm ent operator w hen you re dealing w ith strings containing em bedded NU L characters. For exam ple, the follow ing code dem onstrates how to use and m isuse this m ethod. CComBSTR str9 ; str9 = OLtSTR ("This works as expected"); // BSTR bstrlnput contains "This is part one\Oand here's part two" CComBSTR strlO ; strlO = bstrlnput; // strlO now contains "This is part one"!!! T he third o p erato r« = () m ethod initializes one CComBSTR object using a LPC - STR pointer to a N U L character-term inated string. T he operator converts the input string, w hich is in A N SI characters, to a U N IC O D E string, then creates a BSTR con- the U N IC O D E string. A gain, w hen you define the (deprecated) preprocessor 0LE2A N SI, the LPCSTR and LPCOLESTR data types are equivalent, so this m ethod has already been defined and m ust not be redefined. tainingl s y m b o l #ifnd€f 0LE2ANSI CComBSTRA operator-(LPCSTR pSrc) { ::SysFreeString(m_str); iUStr m A2WBSTR(pSrc); return *this; > #endif T he final initialization m ethods are tw o overloaded m ethods called S t r i n g . bool LoadString(HINSTANCE hlnst, UINT nI O ) ; bool LoadString(UINT n IO ); T he first loads the specified string resource nID from the specified m odule h ln s t (using the instance handle). T he second loads the specified string resource nID from the current m odule using the global variable _pM odul e. T he _pM odul e global variable points to this m odule’s CCom M odul e-derived in­ stance variable. Y ou initialize the variable w hen you call the C C om M odule:: In it m ethod. Y ou typically call CC om M odul e :: In i tin D ll M ai n (fo r D L Ls) o r W i nM ai n (for EX Es). W atch out though. T he other CComBSTR m ethods w ork quite w ell in ATL SMART TYPES a m odule that has no global CCom M odul e object. H ow ever, using the L o ad S tri ng (U IN T nID ) m ethod requires not only that you have a global instance of CCom­ M odul e but th at you’ve also initialized it. A sk m e how I k n o w ... CComBSTR Operations T here are four m ethods w hich give you access, in varying w ays, to the internal BSTR string that is encapsulated by the CComBSTR class. T he o p e ra to r BSTR() m ethod allow s you to use a CCom BSTR object w here a raw BSTR pointer is required. Y ou in­ voke this m ethod any tim e you cast, im plicitly or explicitly, a CCom BSTR o bject to a B S T R . operator BSTR() const { return nustr; } Frequently, you’ll invoke this operator im plicitly w hen you pass a CCom BSTR ob­ ject as a param eter to a function that expects a BSTR. T he follow ing code dem on­ strates this: HRESULT p u t—Name ( / * [ i n ] */ BSTR pNewValue); CComBSTR bstrName = OLESTR ("Frodo Baggins"); pObj->put_Name (bstrName); // Implicit cast to BSTR The o p erato r& () m ethod returns the address of the internal m 一 s tr variable w hen you take the address of a CCom BSTR o b ject U se care w hen taking the address of a CComBSTR object. B ecause the o p erato r& () m ethod returns the address of th e in tern al BSTR variable, you can overw rite th e in tern al variable w ith o u t first free­ ing the string. T his causes a m em ory leak. BSTH* operatorAO { return A«*_str; } T his o p erato r is quite useful w h en you are receiving a BSTR pointer as th e o u tp u t of som e m ethod call. Y ou can sto re the returned BSTR directly into a CCom BSTR object so the o bject m anages the lifetim e o f the string as follow s: HRESULT get_Ne.me (/* [out] */ BSTR* pName); CComBSTR bstrName ; get_Name (AbstrName); // bstrName empty so no memory leak ATL INTERNALS T he C opyT o m ethod m akes a duplicate of the string encapsulated by a CCom­ BSTR o b ject and copies the duplicate’s BSTR pointer to th e specified location. Y ou w ill need to free the returned BSTR explicitly by calling S y sF re e S tri ng. HRESULT CopyTo(BSTR* pbstr) { • • • } T his m ethod is handy w hen you need to return a copy of an existing BSTR property to a caller. F o r exam ple: STDMETHODIMP SomeClass::get_Name (/* [out] */ BSTR* pName) { // Name is maintained in variable m_strName of type CComBSTR return m_strName.CopyTo (pName); > T he D etach m ethod returns the BSTR contained by a CComBSTR object. It em p­ ties th e object so th e d estru cto r w ill n o t attem p t to release the internal BSTR. Y ou w ill need to free th e returned BSTR explicitly by called S y sF re e S tri ng. BSTR DetachO { BSTR s » nustr; nustr - NULL; return s; } Y ou w ould use this m ethod w hen you have a string in a CComBSTR object th at you w ish to retu rn to a caller an d you n o lo n g er n eed to k eep th e string. In th is situation, using the C opyT o m ethod w ould be less efficient because you w ould m ake a copy of a string, retu rn the copy, th en discard th e original string. U se D etach as follow s to retu rn th e original strin g d irectly : STDMETHODIMP SomeClass::get.Label (/* [out] */ BSTR* pName) { CComBSTR strLabel; // Generate the returned string in strLabel here *pName * strLabel.Detach (); return S_OK; } T he A ttach m ethod perform s the inverse operation. It takes a BSTR and at­ tach es it to an em pty CCom BSTR object. O w nership of th e BSTR now resid es w ith th e CCom BSTR object and the object’s destructor w ill eventually free the string. N ote th at you sh o u ld n o t attach a BSTR to a non-em pty CCom BSTR o b ject If you do, you'll receive an assertion at runtim e w hen running a debug build. H ow ever, a release build w ill silently leak m em ory. ATL SMART TYPES void Attach(BSTR src) { ATLASSERT(m_str •• NULL); nustr • src; } U se care w hen using the A ttac h m ethod. Y ou m ust have ow nership of the BSTR you are attaching to a CCom BSTR object because eventually the object w ill attem pt to destroy the BSTR. F or exam ple, the following code is incorrect: STDMETHODIMP SomeClass::put^Name (/* [in] */ BSTR bstrName) . { // Name is maintained in variable m_strName of type CComBSTR m_strName.Empty( ); istrName.Attach (bstrName); // Wrong! We don't own bstrNaae return E_BONEHEAD; M ore often, you’ll use A ttac h w hen you’re given ow nership of a BSTR and you w ish a CCom BSTR object to m anage the lifetim e of the string. STDMETHODIMP SomeClass::get—Name (/* [out] */ BSTR* pName); • • • BSTR bstrName; pObj->get_Name (AbstrName); // We own and must free the raw BSTR CComBSTR strName; strName•Attach(bstrName); // Attach raw BSTR to the object W hen a CCom BSTR object to w hich you w ish to attach a BSTR m ight already con­ tain a BSTR, call the E m pty m ethod first. T he E m pty m ethod releases any internal BSTR and sets the m _ str m em ber variable to NULL. T he S y sF reeS tri ng f u n c t i o n explicitly docum ents th at the function sim ply returns w hen the input param eter is NULL so you can call E m pty on an em pty object w ithout problem . void Empty() { ::SysFreeStringCm^str); m^str » NULL; } String Concatenation Using CComBSTR Six m ethods co n caten ate a specified string w ith a CCom BSTR object: fo u r overloaded A ppend m ethods, one A ppendBSTR m ethod and the o p erato r+ * () m ethod. HRESULT Append(LPCOLESTR Ipsz, int nlen) HRESULT App«nd(Lf>COLESTR lpsz); fifndef 0LE2ANSI ATL INTERNALS HRESULT Append(LPCSTR); # e n d if HRESULT Append(const CCom8STR& b s tr S r c ) ; CComBSTR& o p € ra to r+ « (co n st CComBSTR* b s trS rc ); HRESULT AppendBSTR(BSTR p); The A ppend (LPCOLESTR lp sz , in t nL en) m ethod com putes the sum of the length of the current string plus the specified nL en value and allocates an em pty BSTR of the co rrect size. It copies the original string into th e new BSTR, th en con­ caten ates nL en ch aracters of th e 1 p sz string onto th e en d of th e new BSTR. Finally, it frees th e original string an d rep laces it w ith th e n ew BSTR. CComBSTR strS entence *= OLESTR(uNow is " ) ; st「Sentence .Append(OLESTR(__the time of day is 0 3 : 00 PM”),9); // strSentence contains "Now is the time •• T he rem aining three overloaded A ppend m ethods all use the first m ethod to perform the real w ork. T hey differ only in the m anner in w hich th e m ethod obtains the string and its length. T he A ppend (LPCO LESTR 1 p sz ) m ethod appends the con­ tents of a N U L character-term inated string of OLECHAR characters. T he A ppend (LPCSTR lp sz ) m ethod appends the contents of a NUL character-term inated string of A N SI characters. T he A ppend (co n st CComBSTRA b strS rc ) m ethod ap­ pends the contents of an o th er CCom BSTR object. F or notational and syntactical con­ venience, the ope ra to 「+«■() m ethod also appends the specified CComBSTR to the cu rren t string. CComBSTR s tr ll (OLESTR("for a ll good men ••)); strSentence.AppendCstrll); / / calls Append(const // strSentence contains "Now is the time for a ll strSentence.Append(OLESTR("to come ••)); / / calls lp s z ); // strSentence contains "Now is the time for all CComBSTR* b s trS rc ) ; good men " Append (LPCOLESTR good men to come" strSentence.Append("to the aid "); / / calls Append (LPCSTR lpsz); / / strSentence contains ATL SMART TYPES // "Now is the time for all good men to come to the aid CComBSTR s t r l2 StrSentence += // "Now is the // country" (OLESTRC'of their country")); s t r l2 ; II calls operator+=() time for all good men to come to the aid of thei W hen you call A ppend using a BSTR param eter, you are actually calling the A ppend (LPCO LESTR 1 p sz) m ethod, because, to the com piler, the BSTR argum ent is an OLECHAR* argum ent. T herefore, the m ethod appends characters from the BSTR until the first N U L character is encountered. W hen you w ant to append the contents of a BSTR that possibly contains em bedded N U L characters, you m ust ex­ plicitly call the A ppendB ST R m ethod. Y ou can’t go w rong follow ing th ese guidelines: ■ When the parameter is a BSTR, use the AppendBSTR method to append the entire BSTR regardless of w hether it contains em bedded N U L characters. ■ W hen the param eter is a LPCOLESTR or a LPCSTR, use the A ppend m ethod to append the N U L character-term inated string. ■ So m uch for function overloading ... Character Case Conversion T he tw o character case conversion m ethods, T oL ow er and T oU pper, convert the internal string to low er case or upper case, respectively. B oth m ethods use the A TL string conversion m acro 0L E 2T to convert, if necessary, a pointer to a string of OLECHAR characters to a pointer to a string of TCHAR characters. T he W in32 functions C harL ow er and C harllpper are used to convert the case of a TCHAR string. Finally, th e m ethods u se th e A TL string conversion m acro 丁2 BSTR to co n v ert the case-converted string back into a BSTR. W hen everything w orks, th e new string replaces the original string as the co n ten ts of the CCom BSTR object. H R E S U L T T o L o w e r () { USES^CONVERSION; i f ( n u s t r !* NULL) { 葉 LPTSTR psz * CharLower(0L£2T(nustr)); if (ps2 NULL) return E^OUTOFMEMORY; BSTR b * T2BSTR(ps2); // if (psz =* NULL) return E.OUTOFMEMORY; // ATL 3.0 BDG! if (b « NULL) return E.0UT0FMEM0RY; ATL INTERNALS SysFreeStri 叫(nustr); mjstr m b; N ote th at the conversion, from OLECHAR characters to TCHAR characters, stops at the first em bedded N U L character regardless of the actual length of the string. T hese m ethods produce a case-converted BSTR that is truncated at the first em - N U L character in the original string. A lso, the conversion is potentially i the sense cannot convert a character w hen the local code page doesn’t contain a c r equivalent to the original U N ICO D E character. CC om BSTR C om parison O perators T he sim plest com parison c^>erator is o p e ra to r! (). It returns tru e w hen the CComBSTR object is em pty and fa l se otherw ise. bool operator I () const { return (oi_str «■ NULL); > There are tw o overloaded versions of both the o p erato r< () and the o p erato r= = () m ethods. A s the code in both m ethods is nearly the sam e, I’ll discuss only the o p erato r< () m ethods. The com m ents apply equally to the operator= = C ) m ethods. A ll these com parison operators are arguably broken to a slight degree. T he basic problem is that they do not support com paring tw o CComBSTR objects that contain strings w ith em bedded N U L characters. T hese com parison operators use the A N SI C runtim e library function w cscm p to perform the com parison. The w cscm p function expects N U L character-term inated strings. T herefore, it stops com paring at th< NU L character it encounters. In addition, cscm p function com pares strings using com parison rules ac­ cording to th e C ru n tim e locale setting. B y default, it is th e “C ” lo cale th at h an d les only the 52 non-accented alpha characters. T o have your string com parisons per- foim ed using th e locale used by th e operating system , call s e tl o c a l e like this: setlocale (LC-ALL, MM) ; In the first overloaded version of the o p e ra to r< () m ethod, th e operator com ­ pares against a provided BSTR argum ent T he function prototype specifies (there­ fore also im plies) th at th is function should com pare against a BSTR, n o t sim ply an OLECHAR*. T herefore, I think it should handle em bedded N U L characters. H ow ever, it doesn’t because it uses th e w cscm p function. ATL SMART TYPES bool operator<(BSTR bstrSrc) const { if (bstrSrc ** NULL SA m—st” *« NULL) return false; if (bstrSrc !« NULL A& irustr !• NULL) return wcscmp (m一 str. bstrSrc) < 0; return m_str ** NULL; } In the second overloaded version of the o p e ra to r< () m ethod, the operator com pares against a provided LPCSTR argum ent. A LPCSTR isn’t the sam e character type as the internal BSTR string, w hich contains w ide characters. T herefore, the m ethod uses the A2W ATL helper m acro to convert the A N SI string to w ide charac­ ters before perform ing the com parison. bool operator<(LPCSTR pszSrc) const { if (pszSrc NULL m^str *= NULL) return false; USES.CONVERSION; i f (ps2 Src !■ NULL && m_str !» NULL) return wcscmp(m_str, A2W (pszSrc)) < 0; return nustr »* NULL; > Strangely, there are no o th er com parison operators. T he CCom BSTR class defines no o p e ra to r> () or o p e ra to r ! = () m ethods. T hey aren’t necessary, of course, but they w ould be convenient and are sim ple to im plem ent. CComBSTR Persistence Support T he last tw o m ethods of the CComBSTR class read and w rite a BSTR string to and from a stream . The W riteT oS tream m ethod w rites an ULONG count containing the num bers of bytes in the BSTR to a stream . It w rites the BSTR characters to the stream im m ediately follow ing the count. N ote th at the m ethod does not tag the stream w ith an indication of the byte-order used to w rite the data. T herefore, as is frequently the case for stream data, a CComBSTR object w rites its string to the stream in a hardw are-architecture specific form at. HRESULT WriteToStream(IStreai«* pStream) { ATLASSERT(pStream !» NULL); ULONC cb ; ATL INTERNALS ULONC cbStrLen * m—str ? SysStringByteLen(m^str)+sizeof (OLECHAR) : 0 ; HRESULT hr * pStream->Write((void*) AcbStrLen, sizeof (cbStrLen), &cb); if (FAILEO(hr)) return hr; return cbStrLen ? pStream->Write((void*) m_str, cbStrLen, & c b ) : S 一 OK; T he R eadF rom S t ream m ethod reads an ULONC count of bytes from the speci­ fied stream , allo cates a BSTR of th e co rrect size, th en read s th e ch aracters directly into the BSTR string. T he CComBSTR object m ust be em pty w hen you call R ead- F rom S t ream , otherw ise you’ll receive an assertion from a debug build o r w ill leak m em ory in a release build. HRESULT ReadFromStream(IStream^ pStream) i ATLASSERT(pStream !» NULL); ATLASSERT(m_str «* NULL); // should be empty ULONC cbStrLen - 0; HRESULT hr * pStream->Read((void*) &cbStrLen( sizeof (cbStrLen), NULL); • if ((hr S_0K) Ur (cbStrLen !« 0)) { //subtract size for terminating NULL that we wrote out //since SysAI1ocStringByteLen overallocates for the NULL nustr - SysAI1ocStringByteLen(NULL, cbStrLen-sizeof(OLECHAR)); if On一 str =** NULL) hr = E_0UT0FMEMORY; else hr * pStrearo->Read((void*) m_str, cbStrLen, NULL); > if (hr — S^FALSE) hr - E.FAIL; return hr; Minor Rant on BSTRs, Embedded NUL Characters in Strings and Life in General T he com piler considers the types BSTR and OLECHAR* as synonym ous. In fact, the BSTR sym bol is sim ply a ty p ed ef for OLECHAR*. For exam ple, from w t y p e s . h : ATL SMART TYPES typedef /* [wire^marshal] V OLECHAR 一 RPC_FAR *BSTR; This is m ore than som ew hat brain-dam aged. A n arbitrary BS i R is not an OLE­ CHAR* and an arbitrary OLECHAR* is not a BSTR. O ne is oft^n m isled on this re­ gard because in frequently occurring situations, a BSTR w orks just fine as an OLECHAR*. STDMETHOOIMP SomeClass::put一 Name (LPCOLESTR pName) ; BSTR bstrlnput =... pObj->put_Name (bstrlnput) ; // This works just fine..’ usually SysFreeStri ng (bstrlnput); In the previous exam ple, the b s trln p u t argum ent, as it’s defined to be a BSTR, can contain em bedded N U L characters w ithin the string. T he p u t 一 Nam e m ethod, w hich is expecting a LPCOLESTR (a N U L character-term inated string), w ill proba­ bly save only the characters preceding th e first em bedded N U L character. In other w ords, it w ill cu t th e strin g short. You also cannot use a BSTR w here an [o u t] OLECHAR* param eter is required. For exam ple: STDMETHODIMP SomeClass: { BSTR bstrOutput =... ★ppName = bstrOutput ; return S_OK ; // } get_Name (/* [out] V OLECHAR** ppName) II Produce BSTR string to return // This compiles just fine but leaks memory as caller doesn't release BSTR C onversely, you cannot use an OLECHAR* w here a BSTR is required. W hen it does happen to w ork, it’s 汪 latent bug. F or exam ple, the following code is incorrect: STDMETHODIMP SomeClass::put_Name (BSTR bstrName); pObj->put_Name (OLECHAR("This is not a BSTR!")) ; // Wrong! Wrong I Should the put_N am e m ethod call S y sS tri ngL en to obtain the length of the BSTR, it’s going to be quite su rp rised as it w ill try to get th e length from the integer p re­ ceding the string and there is no such integer. T hings get w orse should the p u t_ N am e m ethod be rem oted, th at is, live ou t of process. In this case, the m arshaling ATL INTERNALS c o d e will call S y sS tri ngL en to obtain the num ber of characters to place in the O R PC request packet. T his is usually a huge num ber (4 bytes from the preceding strin g in th e literal pool in th is ex am p le) and often cau ses a G P F w hile trying to copy th e string. B ecause the com piler cannot tell the difference betw een a BSTR and an OLECHAR*, it’s quite easy to accidentally call a m ethod in CCom BSTR th at doesn’t w ork correctly w hen you are using a BSTR containing em bedded N U L characters. So the follow ing discussion show s exactly w hich m ethods you m ust use for these kinds of BSTRs. T o construct a CCom BSTR, you m u st specify the length of th e string: BSTR bstrlnput = SysAllocStringLen (OLESTR (''This is part one\0and here's part two”), 36); CComBSTR str8 (bstrlnput) ; // Wrong! Unexpected behavior here // Note: str8 contains only "This is part one' CComBSTR st「9 (::SysStringLen (bstrlnput), bstrlnput); // Correct! II str9 contains "This is part one\0and here’s part two" A ssigning a BSTR containing em bedded N U L characters to a CComBSTR object never w orks. For exam ple: II BSTR bstrlnput contains "This is part one\0and here's part two" CComBSTR strlO ; strlO = bstrlnput; // Wrong! Unexpected behavior here // strlO now contains .’Th i s is part one” !!! T he easiest w ay to perform an assignm ent of a BSTR is to use the E m pty and A ppendBSTR m ethods : strlO.Empty (); // Insure object is initially empty strlO.AppendBSTR (bstrlnput); // This works! B e careful using the To L ow er and T oU pper m ethods. T hey truncate the case- converted string at the first em bedded N U L character. A lso, the com parison opera­ tors, o p erato r< () and o p erato r= = (), only com pare characters up to the first em bedded N U L character. In practice, w hile a BSTR can potentially contain zero or m ore em bedded N U L ch aracters, m ost o f th e tim e it doesn’t. T his, o f course, m eans th at m o st of the tim e, you don’t see the laten t bugs caused by incorrect BSTR usage. ATL SMART TYPES The ComVariant Smart VARIANT Class A Review of the COM VARIANT Data Type O ccasionally, w hile using CO M , you’ll w ant to pass param eters to a m ethod w ithout any know ledge of the d ata types the m ethod requires. F or the m ethod to be able to interpret its received param eters, the caller m ust specify the form at of the d ata as w ell as its value. A lternatively, you m ay call a m ethod th at retu rn s a result consisting of varying data types depending on context. S om etim es it returns a string, som etim es a 1 ong, or even an interface pointer. T his requires the m ethod to return data in a self­ describing data form at. F or each value transm itted, you w ould send tw o fields: a code specifying a data type and a value represented in the specified data type. C learly, for this to w ork,the sen d er and receiver m ust agree on the set of possible f o r m a t s . CO M specifies one such set of possible form ats (the VARTYPE enum eration) and specifies the structure that contains both the form at and an associated value (the VARIANT structure). T he VARIANT stru ctu re looks like this: typedef struct FARSTKUCT tagVARIANT VARIANT; typedef struct FARSTRUCT tagVARIANT VARIANTARC; typedef struct tagVARIANT { VARTYPE vt; unsigned short wReservedl; unsigned short wReserved2; unsigned short wReservedB; union { unsigned char bVal; short iVal; long IVal; float fltVal; double dblVal; VARIANT_BOOL boolVal; SCOOE scode; CY cyVal; DATE date; BSTR bstrVal; IUnknown FAR* punkVa1; IDispatch FAR* pdispVal 5AFEARRAY FAR* parray; unsigned char FAR* pbVal; .UIl •12 VT」 「一 DISPATCH VTJVRRAYI* VT3YREFIVT.UI1 ATL INTERNALS double VARIANT^BOOL short long float FAR* pfltVal; FAR* pdblVal; FAR* pboolVal; FAR* pscode; FAR* pcyVal; FAR* pdate; FAR* piVal; FAR* piVal; VT_BYREF|VT«I2 VT^BYREF|VT_I4 VT^BYREF|VT_R4 VT_BYREF|VT«R8 SCOOE CY V T 3 Y R E C |VT.B00L VT_BYREF|VT_ERROR DATE BSTR IUnknown FAR* IOi spatch FAR* SAFEARRAY FAR* VARIANT void FAR* pbstrVal; FAR* ppunkVal; FAR* pparray; FAR* pvarVal; FAR* byref; FAR* ppdispVal; Vr_BYREF|VTJ)ATE VT_BYREF|VT_BSTR VT 一 BYREFI VT^UNKNOWN VT3YREF | VTJ)I5PATCH VTLARRAYI* VT^BYREF|VT-VARIANT Generic ByRef Y ou initialize th e V A RIA N T stru ctu re by storing a value into o n e o f th e fields of th e tagged union and then storing the correspon iing tyoe for the value into the v t m em ber of the VARIANT structure. T he VARIANT data type has a num ber of sem an­ tics th at m ake using it tedious and error-prone for C + + developers. A VA RIAN T has th e follow ing attributes. ■ A VARIANT m ust be initialized before use by calling th e V ari a n tln i t function on it A lternatively, you can initialize th e type and associated value field to a valid state. ■ A VARIANT m ust be copied by calling th e V ari an tC o p y function on it T his per­ form s the proper shallow or deep copy, as 叩 propriate for th e d ata type stored in the VARIANT. ■ A VARIANT m ust be destroyed by calling the V ari antC I ear function on it T his perform s the pro p er shallow or deep destroy, as appropriate for the data type stored in the VARIANT. F or exam ple, w hen you destroy a VARIANT con­ taining a SAFEARRAY of BSTRs, V ari an tC I e a r frees each BSTR elem ent in the array, th en frees th e array itself. ■ A VA RIAN T can optionally rep resen t, a t m ost, one level o f indirection, w hich is specified b y th e addition o f the VT_ARRAY b it settin g to th e type code. Y ou can call V ari an tC o p y ln d to rem ove a single level of indirection from a VARIANT. _ You can attem pt to change the data type of a VARIANT by calling V ari a n t- ChangeType[Ex]. W ith all th ese special sem antics, it w ould b e useful to en cap su late th ese details in a reusable class. A TL provides such a class : CC om V ari an t. ATL SMART TYPES The CComVariant Class T he C C om V ari a n t class is an A TL utility class th at is a useful en capsulation for the CO M self-describing data type, VARIANT. T he a tlb a s e .h file contains the defini­ tion of th e C C om V ari a n t class. T he only state m aintained by th e class is ai» instance of the VARIANT structure, w hich the class obtains by inheritance from the ta g ­ VARIANT structure. T his, conveniently, m eans a C C om V ari a n t instance is a V A R I­ ANT structure, so you can pass a C C om V ari a n t instance to any function expecting a VARIANT structure. class CComVariant: public tagVARIANT { Y ou’ll often use a C C om V ari a n t instance w hen you need to pass a VARIANT ar­ gum ent to a CO M m ethod. T he follow ing code passes a string argum ent to a m ethod that expects a VARIANT. T he code uses the C C om V ari a n t class, so it doesn’t need to deal w ith the m essy sem antics of a BSTR. STDMETHODIMP put__Name(/* [in]* const VARIANT* name); HRESULT SetName (LPTSTR pszName) { // Initializes the VARIANT structure II Allocates a BSTR copy of pszName II Sets the VARIANT to the BSTR CComVariant v (pszName); II Pass the raw VARIANT to the method Return pObj->put_Name(&v); // Destructor clears v, freeing the BSTR } Constructors and Destructor T here are 16 constructors available for CC om V ari a n t objects. T he default con­ stru cto r sim ply initializes th e VA RIANT to EM PTY, w hich says th e VA RIAN T contains no inform ation. T he destructor calls the C l e a r m em ber function to release any reso u rces potentially held in the VARIANT. CComVariant() { vt • VT_EHPTy; } -CComVariant() { Clear (); } ATL INTERNALS T he oth er 15 co n stru cto rs initialize the VARIANT stru ctu re appropriately, based on the type of the constructor argum ent. M any of the constructors sim ply set the v t m em ber of the VARIANT structure to the value representing the type of the constructor argum ent and sto re the value of the argum ent into the appropriate m em ber of the union. CComVariant(int nSrc) { vt :: VT..14; CComVariant(BYTE nSrc) { vt :• VT.-un; CComVari antCshort nSrc) { vt * VT_.12; CComVariant(float fltSrc) { vt :: VT.•R4; CComVariant(double dblSrc) { vt a VT..R8; CComVariantCCY cySrc) { vt »« VT.•CY; lVal « nSrc; } bVal « nSrc; } fltVal « fltSrc; } dblVal - dblSrc; } cyVal.Hi * cySrc.Hi ; cyVal.Lo * cySrc.Lo ; } A few of th e co n stru cto rs are m ore com plex. A n SCODE looks like a 1 ong to th e com piler. T herefore, constructing a C C om V ari a n t specifying an SCODE or a lo n g initialization value invokes the co n stru cto r accepting a 1 ong. T o p erm it you to dis­ tinguish these tw o cases, this co n stru cto r also tak es an optional argum ent allow ing you to specify w hether the 1 ong should be placed in the VARIANT as a 1 ong o r as an SCODE. W hen you specify a variant type other than VT 一 14 o r VT^ERROR, this constructor asserts in a debug build. W indow s 16-bit C O M defined an HRESULT (a handle to a result code) as a data type that contained an SCODE (a status code). T herefore, you’ll occasionally see o ld er legacy co d e th at co n sid ers th e tw o d ata ty p es to be different. In fact, th ere are obsolete m acros that convert an SCODE to an HRESULT and th at extract the SCODE from an HRESULT. H ow ever, the SCODE and HRESULT data types are identical in 32-bit CO M applications. T he VARIANT data structure contains an SCODE field, rath er than an HRESULT, because th at’s the w ay it w as originally declared. CComVariant(long nSrc, VARTYPE vtSrc - VT_I4); The constructor accepting a bool initialization value sets the contents of the VARIANT to VARIANT—TRUE or VARIANT—FALSE, as appropriate, not to the b o o l value specified. A logical TR U E value, as represented in a VARIANT, m ust be VARIANT 一 TRUE (-1 as a 16-bit value), and logical FA LSE is V A RIA N T.FA LSE (0 as a 16-bit value). T he C + + language defines the b o o l data type as an 8-bit (0 o r 1) value. T his co n stru cto r provides the conversion betw een the tw o representations of a B oolean value. CComVariant(bool bSrc) { vt 篇 VT_B00L; boolVal • bSrc ? VARIANT.TRUE : VARIANT.FALSE ; > ATL SMART TYPES T w o co nstructors accept an interface pointer as an initialization value and p ro ­ duce a C C om V ariant instance that contains an A ddR ef’ed copy of the interface pointer. T he first constructor accepts an ID isp atch * argum ent. T he second ac­ cepts an IU nknow n* argum ent. CComVariant(IDispatch* pSrc); CComVariant(IUnknown* pSrc); T w o co n stru cto rs allow you to initialize a CCom V a ri a n t instance from another VARIANT structure or CCom V a「i a n t instance. CComVariant(const VARIANTA varSrc) { vt « VT_EMPTY; InternalCopy (AvarSrc); > CComVariant(const CComVariant& varSrc); { Same as above } T hey both have identical im plem entations, w hich have a subtle side effect, so let’s look at the In te rn a l C opy helper function used by both constructors. void Interna1Copy(const VARIANT* pSrc) { HRESULT hr » Copy(pSrc); if (FAILED(hr)) { vt « VT 一 ERROR; scode =* hr; } } N otice how In tern alC o p y attem pts to copy the specified VARIANT into the in­ stance being constructed; w hen the copy fails, In te rn a lC o p y initializes the CCom - V ari a n t instance as holding an error code (VT—ERROR). T he actual erro r code is that returned by the C opy m ethod used to attem pt the copy. T his seem s like an odd approach until you realize that a co n stn irto r cannot return an error, sh o rt of th ro w ­ ing an exception. ATL doesn't require support for exceptions, so this constructor m ust initialize the instance even w hen the C opy m ethod fails. I once had a CComVa ri an t instance that alw ays seem ed to have a VT_ERROR code in it,even w hen I thought it sh o u ld n ’t* A s it tu rn ed out, I w as co n stru ctin g the CCom Va ri a n t instance from an uninitialized VA RIAN T stru ctu re th at resided on the stack. W atch o u t for code like this: void func () { // The following code is incorrect VARIANT v; // Uninitialized stack garbage in vt member CComVariant vv (v); // Indeterminate state - with luck, VT_ERROR ATL INTERNALS T hree constructors accept a string initialization value and produce a CCom­ V ari a n t instance th at contains a BSTR. T he first constructor accepts a BSTR argu­ m ent and creates a C C om V ari a n t containing a BSTR th at is a copy of the specified BSTR. T he second accepts a LPCOLESTR argum ent and creates a C C om V ari a n t con­ tain in g a BSTR th at is a co p y of th e specified strin g o f OLECHAR ch aracters. T he th ird accepts a LPCSTR argum ent and creates a C C om V ari a n t containing a BSTR th at is a converted-to-O L ECHAR copy of the specified A N SI ch aracter string. W hen you de­ fine the sym bol 0LE2A N SI, the LPCOLESTR type is the sam e as the LPCSTR type. In th at case, A TL m ust n o t define the co n stru cto r again. T hese th ree co nstructors also have th e p o ssibility of “failing.” In all th ree co n stru cto rs, w h en th e co n stru cto r can­ n ot allocate m em ory for the new BSTR, it initializes the C C om V ari a n t instance to VT 一ERROR w ith SCODE E_OUTOFMEMORY. CComVariant(BSTR bstrSrc); CComVariant(LPCOLESTR IpszSrc); fifndef 0LE2ANSI CComVariant(LPCSTR IpszSrc); fendif It’s som ew hat strange th at there is a separate co n stru cto r for a BSTR argum ent and for a LPCOLESTR argum ent. B ecause of a brain-dam aged ty p ed ef, ranted ab o u t previously in the ch^D ter, th e only difference b etw een th e tw o d ata types is a c o n st type specifier. That is^ a^BSTR is an OLECHAR*, w hereas a LPCOLESTR is a c o n st OLECHAR*. T his m eans th at you usually aren’t calling the constructor you m ight think you’re calling. void func () { BSTR bstrlnput - : rSysAllocString (OLESTR ("This is a BSTR string*')); CComVari ant vl (bstrlnput); // calls CCoaVariant (BSTR) // calls CCoaVaHant (BSTR) CComVari ant v2 COLESTRC'This is an OLECHAR string")); OLECHAR* ps » OLESTRC'This is another OLECHAR string"); CComVariant v3 (ps); // calls CCoaVariant (BSTR) const OLECHAR* pcs - OLESTRC'This is another OLECHAR string'*); CComVariant v4 (pcs); // Only this calls CCo«Var1ant (LPCOLESTR) ::SysFreeString (bstrlnput); ATL SMART TYPES The C C om V ariant (BSTR) constructor also doesn’t properly handle a BSTR con­ taining em bedded NUL characters. It produces a C C om V ariant initialized w ith a BSTR th at contains only the characters preceding the first em bedded NUL. In sum ­ m ary, this co n stru cto r provides no functionality beyond w hat is already available in the C om V ariant (LPCOLESTR) constructor. Initialization The C C om V ariant class defines 15 assignm ent operators. All the assignm ent o p e r a t o r s ■ C lear th e VA RIAN T of its cu rren t contents. S et the v t m em ber of the VARIANT structure to the value representing the type of the assignm ent operator argum ent. S tore the value of the argum ent into the appropriate m em ber of the union. CCom Variant& operator**(int nS rc); CCom V ariant* operator»(8Y TE nS rc); CCom Variant& operator= *(short n S rc); CCom V ariant* operator»(long nS rc); CCom Vari ant& operator=*(C Y cyS rc) // Creates V T J 4 variant // Creates VT_UI1 variant // Creates V T 一 U 2 variant // Creates V T 一 1 4 variant // Creates VT 一 R4 variant II Creates VTJ18 variant II Creates v r jy variant T he rem aining o p e ra to r: m ethods have additional sem antics. Like the equiv­ alent constructor, the assignm ent operator accepting a bool initialization value sets the contents of the VARIANT to VARIANT_TRUE o r VA RIAN T^FALSE, as appro­ priate, n o t to the b o o l value specified. CComVariant& operator*(bool bSrc); T w o assignm ent operators accept an interface pointer and produce a CCom­ V a ria n t instance that contains an A ddR ef’ed copy of the interface pointer. O ne assignm ent operator accepts an ID i sp atch * argum ent. A nother accepts an IU n­ know n* argum ent. CComVariant& operator«(10ispatch* pSrc); CComVariant& operator-(IUnknown* pSrc); T w o assignm ent o perators allow you to initialize a C C om V ari a n t instance from another VARIANT structure or CC om V ari a n t instance. T hey both use the In te r­ n al C opy m ethod, described previously, to m ake a copy of the provided argum ent. T herefore th ese assignm ent o perators can “fail” and p roduce an instance initialized to the VT_ERROR type. CComVariant& operator«(const CCo«Variant& varSrc); CComVariant^ operator*(const VARIANTS varSrc); T he rem aining three assignm ent operators accept a string initialization value and produce a CCom Va ri a n t instance th at contains a BSTR. T he first constructor accepts a BSTR argum ent and creates a CCom Va ri a n t containing a BSTR th at is a copy of the specified BSTR. T he second accepts an LPCOLESTR argum ent and cre­ ates a CCom V aH a n t containing a BSTR that is a copy of the specified string of OLECHAR characters. T he third accepts an LPCSTR argum ent and creates 往 C C o m _ V ari a n t containing a BSTR th at is a converted-toO LEC H A R copy of the specified A N SI character string. CComVariant* operator«(BSTR bstrSrc); CCo«nVariant& operator.(LPCOLESTR 1 pszSrc); #ifndef 0LE2ANSI CComVariantA operator-(LPCSTR IpszSrc); #endif T he rem ark s m ade previously ab out th e co n stru cto rs w ith a string initialization value apply equally here to the assignm ent operators w ith a string initialization value. In fact, the constructors actually use th ese assignm ent o p erato rs to perform th eir in itializatio n . ■ T hese assignm ent operators can “fail,” producing a CCom V a ri a n t instance ini­ tialized to th e VT^ERROR type. ■ There is no m eaningful distinction betw een the operator= (B S T R ) m ethod and the operator»(LPC O LE STR ) m ethod. ■ The o p e ra to rs (BSTR) m ethod expects a N U L-character-tenninated string. T herefore, it only copies the characters preceding the first em bedded NUL c h a r a c t e r . CComVariant Operations It’s im portant to realize that a VARIANT is a resource th at m ust be m anaged prop­ erly. Ju st as m em ory from th e heap m u st b e allocated an d freed, a VA RIAN T m ust be initialized an d cleared. Ju st as th e ow nership of a m em ory block m ust be explicitly m anaged so it’s freed only once, the ow nership of the contents of a VARIANT m ust also be explicitly m anaged so it’s cleared only once. F o u r m ethods give you control over any resources ow ned by a C C om V ari a n t instance. ATL INTERNALS ATL SMART TYPES T he C l e a r m ethod releases any resources the instance m ay contain by calling the V a ria n tC le ar function. For an instance containing, for exam ple, a lo n g or sim ilar scalar value, this m ethod does nothing. H ow ever, for an instance containing a BSTR, the m ethod releases the string. F or an instance containing an interface pointer, the m ethod releases the interface pointer. F or an instance containing a SA FEAR RAY, th is m eth o d iterates o v er each elem en t in th e array, releasing each ele­ m ent, and then releases the SAFEARRAY itself. HRESULT Clear() { return ::VariantClear(this); > W hen you no longer need the resource contained in a C C om V ari a n t instance, you should call the C l e a r m ethod to release it. T he d estructor does this autom atically for you. So, if an in stan ce w ill quickly go o u t of sco p e after y o u ’re finished using its resources, let the d estru cto r take care of the cleanup. H ow ever, a C C om V ari a n t in­ stan ce as a global o r static variable d o esn ’t leave scop e for a potentially long tim e. T he C l e a r m ethod is useful in this case. T he C opy m ethod m akes a unique copy of the specified VARIANT. T he C opy m ethod produces a C C om V ariant instance that has a lifetim e separate from the lifetim e of th e V A RIA N T th at it copies. HRESULT Copy(const VARIANT* pSrc) O ften you’ll use the C opy m ethod to copy a VARIANT that you receive as an [in ] param eter. T he caller providing an [in ] param eter is loaning the resource to you. W hen you w ant to hold the param eter longer than the scope of the m ethod call, you’ll need to copy th e VARIANT. STDMETHODIMP SomeClass::put—Option (/* [in] */ const VARIANT* pOption) { // Option saved in member m_Option of type CComVariant return m_varOption.Copy (pOption); > W hen you w ant to transfer ow nership of the resources in a C C om V ariant in­ stance from the instance to a VARIANT structure, use the D etach m ethod. It clears the destination VARIANT structure, d a m em cpy of the C C om V ariant instance into the specified VARIANT structure, then sets the instance to VT_EM PTY. N ote that this technique avoids extraneous m em ory allocations and A ddR ef/R el ease c a l l s . { return ::VariantCopy(this,const_cast(pSrc)); } ATL INTERNALS HRESULT Detach(VARIANT* pDest); Do not use the D e t a c h method to update an [out] VARIANT argument without special care! A n [o u t] param eter is uninitialized on input to a m ethod. T he D etach m ethod clears the specified VARIANT before overw riting it. C learing a VARIANT filled w ith random bits produces random behavior. STDMETHODIMP SomeClass::get_0ption (/* [out] */ VARIANT* pOption) { CComVariant va「Option ; • . . Initialize the variant with the output data // Wrong! The following code can generate an exception, corrupt // your heap, and give at least seven years' bad luck! return varOption.Detach (pOption); } B efore detaching into an [o u t] VARIANT argum ent, b e sure to initialize th e output a r g u m e n t . // Special care taken to initialize [out] VARIANT ::VariantInit (pOption) ; |or| pOption->vt - VT^EMPTY ; return varOption.Detach (pOption); // Now we can Detach safely. W hen you w ant to transfer ow nership of the resources in a VARIANT structure from the structure to a CCom Var"i a n t Instance, use the A ttac h m ethod. It clears the current instance, does a m em cpy of th e specified VARIANT into the current in­ stance, and th en sets th e specified VARIANT to VT—EM PTY. N ote th at th is technique avoids extraneous m em ory allocations and A ddR ef/R el e a se calls. HRESULT Attach(VARIANT* pSrc); C lient code can use the A ttach m ethod to assum e ow nership of a VARIANT that it receives as an [o u t] param eter. T he function providing an [o u t] param e> ter is tran sferrin g ow nership o f th e reso u rce to th e caller. STDMETHODIMP SomeClass::get_Opti on (/* [out] */ VARIANT* pOption); void VerboseCetOption () { ATL SMART TYPES VARIANT v; pObj->get_Option (&v); CComVariant cv; cv.Attach (&v); // Destructor now releases the VARIANT Y ou could, som ew hat m ore efficiently, but potentially m ore dangerously, code this differently: void Fragi1eCetOption() { CComVariant v; // This is fragile code!! pObj->get_Option (&v) ; // Directly update the contained VARIANT II Destructor now releases the VARIANT N ote th at in this ease, the g e t 一 O p tio n m ethod overw rites the VARIANT structure contained in the C C om V ariant instance. B ecause the m ethod expects an [o u t] param eter, the g e t 一O p tio n m ethod w ill not release any resources contained in the provided argum ent. In the preceding exam ple, the instance w as freshly con­ structed, so it is em pty w hen overw ritten. T he follow ing code, how ever, causes a m em ory leak. void LeakyCetOptionO { CComVariant v (OLESTR ("This string leaks!")); pObj->get—Option (&v) ;// Directly updates the contained VARIANT II Destructor now releases the VARIANT } W hen you use a C C om V ariant instance as an [o u t] param eter to a m ethod ex­ p ectin g a V A RIA N T, you m u st first clear th e in stan ce if th ere is any possibility th e in­ stance is not em pty. void NiceGetOption() { CComVariant v (OLESTR ("This string doesn't leak!")); v.Cl ear (); pObj->get_Option (&v) ;// Directly updates the contained VARIANT // Destructor now releases the VARIANT ATL INTERNALS The C hangeT ype m ethod converts a C C om V ariant instance to the new type specified by the vtN ew param eter. W hen you specify a second argum ent, C hangeT ype uses it as the source for the conversion. O therw ise, C hangeT ype uses the C C om V ari a n t instance as the source for the conversion and perform s the con­ version in place. HRESULT ChangeType(VARTYPE vtNew, const VARIANT* pSrc - NULL); C hangeT ype converts betw een the fundam ental types (including num eric-tostring and string-to-num eric coercions). C hangeT ype coerces a source that contains a reference to a type (th at is, the VT 一BYREF bit is set) to a value by retrieving the referenced value. C hangeT ype alw ays coerces an object reference to a value by re­ trieving the object’s V al ue property. T his is the property w ith th e D ISPID _V A LU E DISPID. C C om V ariant C om parison O perators The o p e ra to r= () m ethod com pares a CCom V ari an t instance for equality w ith th e specified VA RIAN T structure. bool operator*«Cconst VARIANTS varSrc) const; bool operator丨•(const VARIANT& varSrc) const; W hen the tw o operands have differing types, the operator returns fa l se. For the basic types, the operator com pares the tw o values for equality. T he operator com ­ pares tw o BSTR operands by com paring their lengths. W hen both BSTRs have equal lengths, the o p erato r com pares all characters for the specified length. T his m eans, unlike the CComBSTR class, that the C C om V ariant o p erato r= = () m ethod cor­ rectly com pares tw o BSTRs th at contain em bedded NUL characters. T he com parison of interface pointers is som ew hat unusual and of questionable usefulness. T he o p erato r directly com pares th e binary values of th e interface point­ ers. T his m eans the com parison is not determ ining th at the tw o interface pointers reference the sam e object identity. CO M perm its an object to hand out different binary values each tim e a client queries it for a specific interface pointer (w ith the exception of a query for the IU nknow n interface). T herefore, tw o VARIANTS containing ID i sp atch pointers referencing the sam e object m ight not com pare for equality. E ven tw o VARIANTS containing IU nknow n pointers referencing the sam e object m ight not com pare for equality. T here is no requirem ent th at the canonical IU nknow n (the one returned by a Q uery In te rfa c e (IID _IU n know n, • • •) call) be placed in a VARIANT w ith type VT_UNKNOWN. T he VARIANT w ith type VT.UNKNOWN can contain any interface pointer cast to an IU nknow n interface pointer. ATL SMART TYPES T h e o p e r a t o r ! = () m ethod returns the negation of the operator==() m ethod, so aU the above com m ents apply equally to o p e r a t o r ! = (). B o t h t h e operator<() and operators() m ethods perform their respective com parisons using the V ariant M ath A PI function V a r C m p . 2 bool operator<(const VARIANT& varSrc) const; bool operator〉(const VARIANTS varSrc) const; CComVariant Persistence Support T he last tw o m ethods of the C C o m V a r i a n t class read and w rite a VARIANT t o a n d from a stream . HRESULT WriteToStreamCIStream* pStream); HRESULT ReadFromStream(IStream* pStream); T h e W r i t e T o S t r e a m m ethod w rites the v t type code to the stream . F or the sim ple types, such as VT 一1 4 ,V T_R8, and sim ilar scalar values, it w rites the value of the VARIANT to the stream im m ediately follow ing the type code. For an interface pointer, W ri t e T o S t r e a m w rites the G U ID CLSID_NULL to the stream w hen the p o in ter is NULL. W hen the interface pointer is not NULL, W ri teT oS tream q u e r i e s th e referen ced object fo r its IP e rsi s t S t ream interface. W hen th e object supports th is in terface, W ri t e T o S t r e a m calls th e C O M 01 eSaveToStream function to save th e o b ject to the stream . W hen the interface p o in ter is n o t NULL and th e object does not support the IP e rsi s t S t ream in terface, W riteToStream f a i l s . N ote: In ATL 3.0, W riteToStream does not attem pt to use the IPersistStream lnit interface, which many objects implement in preference to IPersi stStream. F or com plex types, including VT 一BSTR, all by-reference types, and all arrays, W r i t e T o S t r e a m attem p ts to convert the value, if necessary, to a BSTR and w rites th e string to th e stream using CComBSTR:: W ri teT oStream . T he Read From St ream m ethod perform s the inverse operation. F irst, it clears the current C C o m V a r i a n t instance. T hen it reads the variant type code from the stream . F or the sim ple types, such as VT 一 1 4 ,V T_R8, and sim ilar scalar values, it read s th e value of th e VARIANT from th e stream . F o r an in terface p o in ter, it calls th e C O M 0 1 eL oadF rom St ream function to read th e ob ject from th e stream , requesting 2A number of operating system functions manipulate VARIANTS. The functions VarAbs, VarAdd, VarAnd, VarCat, VarCmp, VarDiv, VarEqv, VarFix, Varldiv, Varlmp, Varlnt, VarHod, VarMul, VarNeg, VarNot, VarOr, VarPow, VarRound, VarSub, and VarXor coUectively comprise the Variant Math API. The only documentation I’ve found for these functions is the function prototypes defined in oleauto.h. ATL INTERNALS t h e IUnknown or ID ispatch interface, as appropriate. W hen OleLoadFrom - S t r e a m r e t u r n s REGDB_E_CLASSNOTREC (usually due to reading CLSID^NULL), ReadFrom Stream silently retu rn s an S_O K statu s. F or all o th er types, including VT_BSTR, all by-reference types, an d all arrays, ReadFrom Stream c a l l s CComBSTR: :R eadFrom Stream to read the previously w ritten string from the stream . T he m ethod then coerces the string back to the original type. The CComPtr, CComQIPtr, and CComDispatchDriver Smart Pointer Classes A Review of Smart Pointers A smart pointer is an o b ject th at b eh av es like a pointer. T hat is, you can use an in­ stance of a sm art pointer class in m any of the places you norm ally use a pointer. H ow ever, using a sm art pointer provides som e advantages over using a raw pointer. F or exam ple, a sm art interface pointer class can ■ R elease the encapsulated interface pointer w hen th e class d estructor executes. ■ A utom atically release its interface pointer during exception handling w hen you allocate the sm art interface pointer on the stack, thus reducing the need to w rite explicit exception handling code. _ R elease th e encapsulated interface pointer before overw riting it during an as­ signm ent operation. ■ C a l l A d d R e f on th e interface pointer received during an assignm ent operation. ■ P rovide different constructors to initialize a new sm art pointer through conve­ nient m echanism s. ■ B e used in m any, but not ail, of the places w here you w ould conventionally use a raw interface pointer. ATL provides three sm art pointer classes: CComPtr, CComQIPtr, and CCom- D ispatchD river. The CCom Ptr class is a sm art C O M in terface p o in ter class. Y ou create instances tailo red fo r a specific ty p e of in terface p o in ter. F o r exam ple, th e first lin e of th e fol­ low ing code creates a sm art I U n k n o w n interface pointer. T he seco n d line creates a s m a r t INam edO bject custom interface pointer. CComPt r punk; CComPtr pno; ATL SMART TYPES T h e C C o m Q I P t r class is a sm arter CO M interface pointer class, w hich does everything C C o m P t r does and m ore. W hen you assign to a C C o m Q I P t r instance an interface p o in ter of a different type than the sm art pointer, th e class calls Q u e r y - I n t e r f a c e o n th e provided interface pointer. CComPtr punk = /☆ Init to some IUnknown* */ ; CComQIPtr pno - punk; // Calls punk->QI (IID_INamedObject, •••) T h e CComDi spatchD ri v e r class is a sm art ID i s p a t c h in terface pointer. Y ou use instances of th is class to retrieve and set an o b ject’s p ro p erties using an ID i s- p a t c h interface. CComVariant v; CComDi spatchDri ver pdisp = /* Init to object's IDispatch* */ ; HRESULT hr = pdisp->CetProperty (DISPID_COUNT, &v); // Get the Count property The CComPtr and CComQIPtr Classes T h e CComPtr and CComQIPtr classes are very sim ilar, w ith the exception of initialization and assignm ent. T herefore, all the follow ing com m ents about the C C o m P t r class apply equally to the C C o m Q I P t r class unless I specifically state o t h e r w i s e . T h e a t l b a s e . h file co n tain s th e definition of b o th classes. T he only state m ain ­ tained by each class is a single public m em ber variable, T* p. template class CComPtr { public: T * P ; tempi ate class CComQIPtr { public: T * d : T he first tem plate param eter specifies the type of the sm art interface pointer. T he second tem plate param eter to the C C o m Q I P t r class specifies the interface ID for the sm art pointer. B y default, it is th e globally unique identifier (G U ID ) associated ATL INTERNALS w ith th e class of the first param eter. H ere are a few exam ples th at use these sm art p ointer classes. T he m iddle three exam ples are all equivalent. CComPtr punk; // Smart IUnknown* CComPtr pno; // Smart INamedObject* CComQIPtr pno; CComQIPtr pno; CComQIPtr pno; CComQIPtr pdisp; Constructors and Destructor A C C o m P t r object can be initialized w ith an interface pointer of the 叩 p r o p r i a t e type. T h at is, a C C om Pt r <1 F oo> o b ject can b e initialized using an I F oo* o r an o th er C C o m P t r < I F o o > object. U sing any other type produces a com piler error. T he de­ fault co n stru cto r initializes th e internal interface p o in ter to NULL. T he other con­ stru cto rs initialize th e internal interface p o in ter to th e specified in terface pointer. W hen the specified value is n o n - N U L L , the constructor calls the AddRef m e t h o d . T he d estru cto r calls th e Rel ease m ethod on a non-N U LL interface pointer CComPtr() { p * NULL; } | CComPtr(T* Ip)家 { if C(p « lp) !« NULL) p-> AddRef O ; } CComPtr(const CComPtr& lp) { if (Cp « Ip.p) !■ NULL) p«-> AddRef(); } -CComPtr() { if (p) p->Re1ease(); > A C C om Q IPtr object can be initialized w ith an interface pointer of any type. W hen th e initialization value is th e sam e type as th e sm art pointer, th e co n stru cto r s i m p l y AddReFs th e provided pointer, like th e C C om Ptr class. H ow ever, specifying a different type invokes the follow ing constructor, w hich queries the provided in­ terface p o in ter fo r th e ap p ro p riate interface: CComQIPtr(IUnknown* Ip) { P« NULL; if (Ip !» NULL) 1p->QueryInterface(*piid, (void **)&p); } A co n stru cto r can never fail. N evertheless, the Q uerylnterface call m a y n o t succeed. T lie C C o m Q I P t r class sets th e in tern al p o in ter to NULL w hen it cannot ob­ tain th e required interface. T herefore, youT l u se code su ch as th e follow ing to test w h eth er th e o b ject initializes: ATL SMART TYPES void func (IUnknown* punk) { CComQIPtr pno (punk); if (pno) { II Can call SomeMethod because the QI worked pno->SomeMethod (); } Y ou can tell w hether the query failed by checking for a NULL pointer, but you can­ not determ ine w hy it fails. T he co n stru cto r doesn’t save the HRESULT from a failed Q uerylnterface c a l l . Initialization T h e C C o m P t r class defines tw o assignm ent operators, the C C o m Q I P t r class defines the sam e tw o plus a third. A ll the assignm ent operators ■ R e l e a s e th e cu rren t in terface p o in ter w hen it’s n o n - N U L L . ■ A d d R e f the source interface p o in ter w hen it’s n o n - N U L L . ■ Save the source interface p o in ter as the cu rren t interface pointer. additional C C o m Q I P t r assignm ent operator queries a n o n - N U L L source in­ terface pointer for the appropriate interface to save. Y ou receive a NULL p o i n t e r w h e n t h e Q uerylnterface calls fails. L ike th e equivalent co n stru cto r,the HRE­ SULT fo r a failed q u ery is n o t available. // CComPtr assignment operators // CComQIPtr assignment ops.. T* operator®(了* Ip); T* operator»(T* Ip); T* operator»(const CCowPtr4 Ip); T* operator- (const CComQIPtrA Ip); T* operator*(IUnknow»n* Ip); T ypically, you’ll use the C C o m Q I P t r assignm ent operator to perform a Q u e r y ­ l n t e r f a c e call. Y ou’ll im m ediately follow th e assignm ent w ith a NULL p o in ter test, as follow s: CComQIPtr m^object; // Member variable holding object STDMETHODIMP put_Object (IUnknown* punk) ATL INTERNALS { // Releases current object, if any, and m_object = punk; // queries for the expected interface if (!m一object) return E_UNEXPECTED; return S_OK; Object Instantiation Methods T he sm art interface pointer classes provide an overloaded m ethod, called C o C r e - atelnstance, th at you can use to instantiate an object and retrieve an interface p o in ter on th e object. T here are tw o form s o f th e m ethod. T he first req u ires th e class id en tifier (CLSID) o f th e class to instantiate. T he seco n d requires th e progranunatic id en tifier ( P r o g I D ) of the class to instantiate. B oth overloaded m ethods accept optional p aram eters for the controlling unknow n and class context for the instanti­ ation. T he controlling unknow n param eter defaults to N U L L — t h e norm al case, in­ dicating no aggregation. The class context param eter defaults to CLSCO CALL, indicating th at any available serv er can service the request. HRESULT CoCreatelnstance (REFCLSID rclsid. LPUNKNOWN pUnkOuter := NULL, / OWORO dwClsContext • CLSCTX.ALL) I ATLASSERT(p mm NULL); return ::CoCreateInstance(rclsidf pUnkOuter , dw»ClsContext, 響 , 灣_ 轉議__ > 一 uuidof(T), (void**)&p); HRESULT CoCreatelnstance (LPCOLESTR szProgID, LPUNKNOWN pUnkOuter »: NULL, OWORO dwClsContext *» CLSCTX^ALL); N otice how the preceding code for the first C oC reatelnstance m ethod creates an instance of the specified class. It passes the param eters of the m ethod to the C oC reatelnstance C O M A PI and, additionally, req u ests th at th e initial interface be the interface supported by the sm art pointer class. (T his is the purpose of the 一 u u i d o f ( T ) expression.) The second overloaded C oC reatelnstance m e t h o d translates the provided ProgID to a CLSID, then creates the instance in the sam e m anner as the first m ethod. T herefore, the follow ing code is equivalent (though the sm art pointer code is easier to read, in m y opinion). T he first instantiation request explicitly uses the ATL SMART TYPES C oC reatelnstance COM A PI. T he second uses the sm art pointer C oC reateln- s t a n c e m e t h o d . ISpeaker* pSpeaker; HRESULT hr = ::CoCreatelnstance (—uuidof (Demagogue), NULL, CLSCDCALL, _ uuidof (ISpeaker, (void**) ApSpeaker); . . . Use the interface pSpeaker->Release (); CComPtr pSpeaker; HRESULT hr = pSpeaker.CoCreatelnstance (一 uuidof (Demagogue)); . . . Use the interface. It releases when pSpeaker leaves scope CComPtr and CComQIPtr Operations B ecause a sm art interface pointer should behave as m uch as possible like a raw interface pointer, th e C C o m P t r class defines som e operators to m a k e t h e C C o m P t r object act like a pointer. F or exam ple, w hen you dereference a p o in ter using o p e r - a t o r * ( ) , you expect to receive a reference to w hatever the pointer points. So dereferencing a sm art interface pointer should produce a reference to w hatever the underlying interface p o in ter points. A nd it does: T& operator*() const { ATLASSERT(p!*NliLL); return *p; } N ote th at the operator*() m ethod kindly asserts w hen you attem pt to derefer­ e n c e a NULL sm art interface p o in ter in a debug build of y o u r com ponent. O f course, I’ve alw ays considered the G eneral P rotection F ault m essage box to be an equiva­ lent assertion. H ow ever, the ATLASSERT m acro produces a m ore program m er- friendly in d icatio n o f th e erro r location. T o m aintain the sem blance of a pointer, taking the address of a sm art pointer object (i.e., invoking o p e ra to 「& ()) should actually return the address of the un­ derlying raw pointer. N ote th at th e issu e h ere isn 't th e actual b in ary value returned. A sm art pointer contains only the underlying raw interface pointer as its state. T herefore, a sm art pointer occupies exactly the sam e am ount of storage as a raw in­ terface pointer. T he ad d ress of a sm art p o in ter o b ject and th e ad d ress o f its internal m em ber variable w ill be the sam e binary value. W ithout overriding CCom Ptr:: operator& (), taking the ad d ress of an in­ stance w ould return a CComPtr*. In order to have a sm art pointer class m ain­ tain the sam e pointer sem antics as a pointer of type T *,the o p e r a t o r & ( ) m e t h o d for the class m u st retu rn a T**. ATL INTERNALS T** operator&O { ATLASSERTCp==NULL); return &p; } N ote th at this operator asserts w hen you take the address of a non-N U LL sm art in­ terface pointer because you m ight dereference the returned address and overw rite the internal m em ber variable w ithout properly releasing the interface pointer. It as­ serts to p ro tect th e sem antics of th e pointer and keep you from accidentally stom p­ ing on the pointer. T his behavior, how ever, keeps you from using a sm art interface pointer as an [ i n , o u t ] function param eter. STDMETHOOIMP SomeClass::UpdateObject (/* [in, out] */ IExpected** ppExpected); CComPtr pE - /☆ Initialize to some value */ ; pobj->UpdateObject C&pE); // Asserts in debug build: pE is non-NULL W hen you really w an t to use a sm art interface p ointer in this w ay, tak e th e address of the m em ber variable: pobj->UpdateObject (&pE•p); CComPtr and CComQIPtr Resource Management Operations. A sm art inter­ face p o in ter represents a resource, albeit one th at tries to m anage itself properly. Som etim es, though, you w ant to m anage the resource explicitly. F or exam ple, you m ust release all interface pointers before calling the C ollni n i t i a l i z e m e t h o d . T his m eans th at you can’t w ait fo r th e d estru cto r of a C C om Pt r o b ject to release the in terface p o in ter w hen y o u allo cate th e o b ject as a global o r static variable (o r even a local variable in m a i n ( ) ) . T he destructor for global and static variables only executes after the m ain function exits, long after C ollni n i t i a l i z e runs. (T hat is, if it runs at all. Y ou m ust link w ith the C + + runtim e library fo r the construc­ tors and destructors of global and static variable to run. A TL itself doesn’t use t h e C / C + + runtim e library; thus, by default, A TL com ponents don’t link w ith the l i b r a r y . ) Y ou can release the internal interface pointer by assigning NULL to the sm art pointer. A lternatively an d m ore explicitly, you can call th e R el e a s e m e t h o d . int main () { " HRESULT hr - Colnitialize (NULL); ATL SMART TYPES If (FAILED (hr)) return -1; // Something is seriously wrong CComPtr punk = /* Initialize to some object */ ; • • ■ punk.Release (); // Must Release before CoUninitialize! CoUni nitializeC); N ote that this code calls the smart pointer object’s CCom Ptr::R elease method b ecause it u ses th e d o t o p erato r to referen ce th e object. It does not directly call the underlying interface pointer's IU nknow n:: Rel ease method as you might expect. The sm art pointer’s CCom Ptr::R elease m ethod calls the underlying i n t e r f a c e p o i n t e r ’s IUnknown::R elease m ethod and sets the internal interface pointer to NULL. T his prevents the destructor from releasing the interface again. H ere is th e sm art pointer’s R el e a s e m e t h o d : template void CComPtr::ReleaseC) { IUnknown* pTemp « p; if (pTemp) { p - NULL; pTemp->Relcase(); > > It’s not im m ediately obvious w hy the CCom Pt r< T > :: R el ease m ethod doesn’t sim ply call IU nknow n:: Rel ease using its p m em b er variable. Instead, it co p ies th e interface p o in ter m em ber variable into the local variable, sets the m em ber variable t o NULL, then releases the interface using the tem porary variable. T his approach avoids a situation in w hich the interface held by the sm art pointer is released t w i c e . F or exam ple, assum e the sm art pointer is a m em ber variable of class A and the sm art p o in ter holds a referen ce to o b ject B. Y ou call th e sm art pointer’s . R e l e a s e m ethod. T he sm art p o in ter releases its reference to o b ject B. O bject B in tu rn holds a reference to the class A instance containing th e sm art pointer. O bject B decides to release its reference to the class A instance. T he class A instance decides to destruct, w hich invokes the d estructor for the sm art pointer m em ber variable. T he d estru cto r d etects th at th e in terface p o in ter is non-N U LL, so it releases th e in terface a g a i n . 3 3Thanks go to Jim Springfield for pointing this out ATL INTERNALS In releases of A TL prior to version 3.0, the follow ing code w ould com pile suc­ cessfully and w ould release the interface pointer tw ice. N ote the use of the arrow o p e r a t o r . In those releases of ATL, the arrow operator returned the underlying interface pointer. T herefore, th e previous line actually called th e IU nknow n:: R elease f u n c ­ tion, n o t the CCom Pt r< T > :: Rel ease m ethod as expected. T his leaves the sm art pointer’s interface pointer m em ber variable n o n - N U L L , so the d estru cto r w ill even­ tually rel< the interface a second tim e. This a nasty bug to find. A sm art pointer class encourages you to think ab o u t an in stan ce as if it w ere an in terface pointer. H ow ever, in th is p articu lar case, you shouldn't use the arrow operator (w hich you w ould if it really w ere a pointer) but m ust use the dot operator because it’s really an o b ject W hat’s w orse, the com ­ piler w ouldn’t tell you w hen you got it w rong. T his changed in version 3.0 of A TL. N ote th at th e cu rren t definition o f th e arrow operator returns a _NoAddRefRel easeOnCComPtr* v a l u e . .NoAddRefRe1easeOnCComPtr* operator->() const { ATLASSERT Cp!«NULL); return (_NoAddRefReleaseOnCComPtr*)p; T his is a sim ple tem plate class w hose only purpose is to m ake the A ddRef and R e l e a s e m ethods inaccessible. template class _NoAddRefReleaseOnCComPtr : public T { punk->Release (); II Wrong! Wrong! Wrong! private ST0METH00.CUL0NC, AddRef)0=0; STDMETHOO.CULONG, Release)Q-0; T h e _NoAddRefRel easeOnCCom Ptr tem plate class derives from th e interface being retu rn ed . T herefore, it in h erits all th e m eth o d s o f th e interface. T he class th en overrides the AddRef a n d R e l e a s e m ethods, m aking them private and pure virtual. ATL SMART TYPES N ow you get the follow ing com piler error w hen you use the arrow operator to call either of these m ethods: error C2248: 'Release' : cannot access private member declared in class 'ATL::_NoAddRefReleaseOnCComPtr1 The CopyTo Method. T h e C o p y T o m ethod m akes an A d d R e f ’e d copy of the in­ terface pointer and places it in the specified location. T herefore, the C o p y T o m ethod produces an interface pointer that has a l i f e t i m e sep arate from the lifetim e of the sm art po in ter th at it copies. HRESULT CopyTo(T** ppT) { ATLASSERT(ppT U NULL); if (ppT « NULL) return E.POINTER; #PPT • p; if (p) p->AddRef(); return S.OK; > O ften, y o u ’ll u se th e C o p y T o m ethod to copy a sm art pointer to an [ o u t ] p a ­ ram eter. A n [ o u t ] interface pointer m ust be A ddR ef^d by the code returning the p o i n t e r . STDMETHODIMP SomeClass::get—Object (/* [out] */ IExpected** ppExpected) { II Interface saved in member m_object of type CComPtr return m_object.CopyTo (ppExpected) ; // Correctly AddRefs pointer } W atch ou t for the follow ing code. It probably doesn’t do w hat you expect and isn’t c o r r e c t . STDMETHODIMP SomeClass::get_Object (/* [out] */ IExpected** ppExpected) { // Interface saved in member m_object of type CComPtr PpExpected = m_object ; // Wrong! Does not AddRef pointer! ATL INTERNALS The Type-Cast Operator. W hen you assign a sm art pointer to a raw pointer, you are im plicitly invoking the o p e r a t o r TO m ethod. In o ther w ords, you are casting th e sm art p o in ter to its underlying type. N otice th at o p e r a t o r T() d o e s n ’ t A d d R e f th e p o in ter it returns: operator T*() const { return (T*) p; } T hat’s because you don’t w ant the A d d R e f in th e follow ing case: STDMETHODIMP SomeClass::put_Object (/* [in] */ IExpected* pExpected); // Interface saved in member m_object of type CComPtr pObj->put_Object (m_object); // Correctly does not AddRef pointer! The Detach and Attach Methods. W hen you w ant to transfer ow nership of the interface pointer in a C C o m P t r instance from the instance to an equivalent raw pointer, use th e D e t a c h m ethod. It retu rn s th e underlying in terface p o in ter and sets th e sm art p o in ter to NULL, ensu rin g th at th e d estru cto r d o esn ’t release th e interface. T he client calling D e t a c h becom es responsible for releasing th e interface. | t * DetachC) { T* pt ■ p; p • NULL; return pt; > You often use D e t a c h w hen you need to return to a caller an interface pointer th at you no longer need. R ather than provide th e caller an A ddR ef’ed copy of the in­ terface, then im m ediately releasing your held interface pointer, you can sim ply transfer the reference to the caller, thus avoiding extraneous A ddRef/Release calls. Y es, it’s a m in o r optim ization, b u t it’s also sim ple. STDMETHODIMP SomeClass::get_Object (/* [out] */ IExpected** ppExpected) { CComPtr pobj » /* Initialize the smart pointer */ ; ♦ppExpected = pobj.Detach(); // Destructor no longer Releases return S_0K; } W hen you w ant to transfer ow nership of a raw interface pointer to a sm art pointer, use the A t t a c h m ethod. It releases the interface pointer held by the sm art pointer, then sets the sm art p o in ter to u se the raw pointer. N ote that, again, this technique avoids extraneous A ddRef/Release calls and is a useful m inor optim ization. ATL SMART TYPES void Attach(T* p2) { i f Cp) p->Release(); p • p2; } C lient code can use the A t t a c h m ethod to assum e ow nership of a raw interface p o in ter th at it receives as an [ o u t ] param eter. T he function providing an [ o u t ] p aram eter is transferring ow nership of th e interface po in ter to th e caller. STDMETHODIMP SomeClass::get_Object (/* [out] */ IExpected** ppObject); void Ve rboseCetOpti on () { IExpected* p; pObj->get_Obj ect (&p ) ; CComPtr pE; pE.Attach (p); II Destructor now releases the interface pointer // Let the exceptions fall where they may now!!! Cal1SomeFunctionWhi chThrowsExceptions(); Miscellaneous Smart Pointer Methods. T he sm art pointer classes also provide useful shorthand syntax for querying for a new interface: the Q uerylnterface m ethod. It takes one param eter 一 th e ad d ress o f a v ariab le th at is o f th e ty p e o f th e desired interface. template HRESULT QueryInterface(Q** pp) const { ATLASSERT(pp !* NULL && *pp »«. NULL); return p->QueryInt€rface(一 uuidof(CD. (void**)pp); > U sing this m ethod reduces the chance of m aking the com m on m istake of querying fo r o n e in terface (e.g., I I D _ I B a r ) b u t specifying a different type o f p o in ter fo r the retu rn ed value (e.g., I F oo*). CComPtr pfoo = /* Initialize to some IFoo */ IBar* pbar; // We specify an IBar variable so the method queries for II0_IBar HRESULT hr » pfoo.QuerylnterfaceC&pbar); ATL INTERNALS U s e t h e I s E q u a l O b j e c t m eth o d to determ ine if tw o interface p o in ters refer to the sam e object. bool IsEqualObject(IUnknown* pother); It p erfo rm s th e test for C O M identity : Q uery each interface for IID 一 I U n k n o w n a n d com pare the results. A CO M object m ust alw ays return the sam e pointer value w hen ask ed for its I U n k n o w n interface. T he IsE qual O bject m ethod expands a little on the C O M identity test. It considers tw o NULL interface p o in ters equal objects. bool SameObjects(IUnknown* punkl, IUnknown* punk2) { CComPtr p (punkl); return p.IsEquaTObject Cpunk2); } IUnknown* punkl ~ NULL; IUnknown* punk2 = NULL; ATLASSERT (SameObjects(punkl, punk2); // true T h e S e t S i t e m eth o d asso ciates a site o b ject (specified by th e p u n k P a r e n t p a ­ ram eter) w ith the object referenced by the internal pointer. T he sm art pointer m ust point to an object that im plem ents the IO b j e c t W i t h S i t e interface. T he A dvi se m ethod associates a connection point sink object w ith the object referenced by the sm art interface pointer (w hich is the event source object). T he first p aram eter is th e sin k interface. Y ou specify th e sin k in terface ID as th e seco n d param eter. T he third param eter is an output param eter. T he A d v i s e m ethod re­ tu rn s a token through th is p aram eter th at uniquely identifies th is connection. HRESULT Advise (I Unknown* pllnk, const IIO& iid, LPOWORD pdw); CComPtr ps f* Initialized via some mechanism */ ; ISomeSink* psink - /* Initialized via some mechanism */ ; DWORD dwCooki e; HRESULT SetSite(IUnknown* punkParent) ps->Advise (psink, — uuidof(ISomeSink), AdwCookie); ATL SMART TYPES 87 T here is no U n a d v i s e sm art pointer m ethod to end the connection because the p o in ter is n o t n eed ed for the U n a d v i s e . T o break the connection, you need only the cookie, th e sink interface identifier (IID), and an event source reference. CComPtr Comparison Operators T hree operators provide com parison operations on a sm art pointer. T he o p e r a ­ t o r ! () m ethod returns t r u e w hen the interface pointer is NULL. The opera- t o r = = ( ) m ethod returns t r u e w hen the com parison operand is equal to the interface pointer. T he o p e r a t o r < ( ) m ethod is rath er useless, in general, because it com pares tw o interface pointers using their binary values. H ow ever, a class needs these com parison operators so that STL collections of class instances w ork p r o p e r l y . bool operator!O const { return (p « NULL); } bool operator< (T* pT) const { return p < pT; } bool operator*«(T* pT) const { return p *■ pT; } N ote that you m ight have to change your coding style for com parisons w hen using sm art pointers. Som e program m ers prefer to w rite equality com parisons to a literal v alu e w ith th e literal o n th e left sid e o f th e ex p ressio n . T his d o esn ’t w o rk w ith the A TL sm art interface classes. F or exam ple: CComPtr pFoo; if (IpFoo) // Tests for pFoo.p == NULL using operator! if CpFoo == NULL) // Tests for pFoo.p == NULL using operator— if (NULL == pFoo) II Does not compile! The CComDispatchDriver Class In ATL 3.0, the CComDi s p a t c h D r i v e r class is m ore or less a specialization of CCom Q IPtr, ex cep t it’s n o t quite as good as it could be. First, it’s an independent class. It doesn’t actually derive from C C o m Q I P t r , T herefore, ail t h e C C o m Q I P t r sm art pointer functionality is im plem ented independently in the CCom D ispatchD river class. U nfortunately, som e of the im provem ents m ade to t h e C C o m Q I P t r class w eren’t propagated to this class. In the next release of A T L , CC om D ispatchD river is typedef'ed as a specialization of C C o m Q I P t r . N ote th at th e state consists of a single m em ber variable, ID i s p a t c h * p . ATL INTERNALS class CComDispatchDriver { public: IDispatch* p; \ T his class contains typical sm art p ointer m ethods, so I’ll only exam ine the ones sig­ nificantly different from th o se discussed for th e CCom Ptr and CCom Q IPtr c l a s s e s . Strange Implementation Quirks Y ou can initialize an instance of the CComDi spatchDri v er class from an ID i s« p a t c h pointer and from an IUnknown pointer. In the latter case, the co n stru cto r w ill query the specified pointer for the ID i spatch interface. U nlike th e CComPtr a n d C C o m Q I P t r classes, you cannot initialize a CComDispatchDriver i n s t a n c e from another CComDi s p a t c h D r i v e r i n s t a n c e . CComOi spatchOri ver(); CComOispatchDriver(IDi spatch^r 1p> ; CComOispatchDriver(IUnknown* lp); N ote th at the operator->() m ethod returns an IDi spatch*. IDispatch* operator->() {ATLASSERT(p!« NULL); return p; } It should return a 一 NoAddRef Rel easeO nCCom Pt r< ID ispatch> pointer, like the o th er sm art pointer classes do. T his m eans you can still use the o p erato r arrow m ethod and call R e l e a s e on the underlying interface pointer w ithout clearing the p m em ber variable. T he d estru cto r later w ill release th e interface p o in ter again. It’s so rt o f m inor, b u t th e o p e r a t o r ! () m ethod returns a BOOL, n o t a b o o l like the other sm art pointer classes return. A BOOL value is a W indow s ty p e d e f f o r a n i n t (a 32-bit value on W in32 system s), typically considered to have the sem antics of nonzero being T R U E , and zero FA LSE. A b o o l value is an intrinsic C + + type th at is a byte value w ith intrinsic t r u e a n d f a l s e v a l u e s . BOOL operator!() {return (p •_ NULL) ? TRUE : FALSE;} Property Accessor and Mutator Methods A few of the m ethods m ake it lots easier to get and set p roperties on an object us­ ing the object’s ID i spatch interface. F irst, you can get th e DISPID fo r a p roperty, given its string nam e, by calling the C etlD O fN am e m ethod. ATL SMART TYPES HRESULT GetIDOfName(LPCOLESTR lpsz, DISPIO* pdispid); O nce you have the DISPID for a property, you can get and set the property’s value using the C etProperty and Put Property m ethods. You specify the DISPID of the property to get or set and send or receive the new value in a VARIANT stru ctu re. HRESULT CetProperty(DISPID dwOispIO, VARIANT* pVar); HRESULT PutProperty(DISPID dwOispIO, VARIANT* pVar): Y ou can skip the initial step and get and set a p ro perty given only its nam e u s­ ing the w ell-nam ed G etPropertyByN am e and PutPropertyByN am e m e t h o d s . HRESULT CetPropertyByName(LPCOLESTR lpsz. VARIANT* pVar); HRESULT PutPropertyByNamedPCOLESTR lpsz, VARIANT* pVar); Method Invocation Helper Functions It’s a royal p ain to call an o b ject’s m eth o d s using th e ID i sp atch :: Invoke m e t h o d . Y ou have to package up all the argum ents into VARIANT stru ctu res, build an array o f t h o s e VARIANTS, and translate the nam e of the m ethod to a DISPID. N othing ex­ trem ely difficult, b u t it’s all ted io u s an d erro r-p ro n e coding. T h e CComDi spatchD ri v e r class has a num ber of m ethods that are custom ized for th e frequent cases for calling an object's m eth o d (s) using ID i s p a t c h . T here are four basic variations: _ C all a m ethod by DISPID or nam e, passing zero param eters. ■ C all a m ethod by DISPID o r nam e, passing one param eter. ■ C all a m ethod by DISPID or nam e, passing tw o param eters. ■ C all a m ethod by DISPID o r nam e, passing an array of N param eters. E ach variation expects the DISPID o r nam e of Uie m ethod to invoke, the argu­ m ents, and an optional return value. HRESULT InvokeOCOISPIO dispid, VARIANT* pvarRet « NULL) HRESULT InvokeO(LPCOLESTR IpszName, VARIANT* pvarRet • NULL) HRESULT Invokel(DISPID dispid, VARIANT、’ pvarParaml, VARIANT* pvarRet * NULL) HRESULT InvokeKLPCOLESTR IpszName, VARIANT* pvarParaml, VARIANT* pvarRet * NULL) ATL INTERNALS HRESULT Invoke2(0ISPID displd, VARIANT* pvarParana, VARIANT* pvarParam2, VARIANT* pvarRet * NULl) HRESULT Invok€2(LPC0tESTR lpszNwe, VARIANT* pvarPararal, VARIANT* pvarParam2, VARIANT* pvarRet - NULL) HRESULT InvokeNCDISPIO d1sp1d( VARIANT* Paraiis, 1nt nParams, VARIANT* pvarftet • NUU) HRESULT InvokeN(L ESTR IpszNawe, VARIANT* pvarParams, int nParamsf VARIANT* pvarRet _ NULL) Finally, there are tw o static m em ber functions, G etProperty and PutProp- e r t y . Y ou can use these m ethods to get and set a property using its DISPID w i t h ­ o u t first attach in g an ID i s p a t c h p o in ter to a CCom D i s p a t c h D r i v e r o b j e c t static HRESULT GetProperty(ICHspatch* pOisp, DISPID (M)1spIO, VARIANT* pVar); static HRESULT PutProperty(HHspatch* p01sp, DISPID dwOispID, VARIANT* pVar); H e r e ’s a n e x a m p l e : HRESULT CetCount (IDispatch* pdisp丨 long* pCount) { ♦pCount » 0; const int DISPID_COUNT - 1; CComVariant v; CComDispatchDriver::GetProperty (pdisp, DISPID一COUNT, &v); HRESULT hr «.v.ChangeType (VT—14); If (SUCCEEDED (hr)) *pCount - V 一1 4 ( & v ) ; return hr; Summary A TL provides a rich set of classes for m anipulating th e d ata types frequently used by CO M program m ers. T he string conversion m acros provide efficient conversion betw een th e various tex t types, but you m ust b e careful not to use them in a loop. ATL SMART TYPES Y ou m ust be especially careful w hen using the BSTR string type because it has num erous special sem antics. T he ATL C C o m B S T R class m anages m any of the special sem antics for you and is quite useful. H ow ever, the class cannot com pensate for the poor decision that, to the C + + com piler, equates the OLECHAR* and BSTR t y p e s . Y ou w ill alw ays need to use care w hen using the BSTR type because the com piler w ill n o t w arn you of m any pitfalls. T h e C C o m V a r i a n t class provides practically the sam e benefits as the C C o m ­ BSTR class, only fo r VARIANT structures. If you use a VARIANT structure, and you w ill need to use one sooner o r later, you should instead use the A TL C C o m V a r i a n t s m a r t VARIANT class. Y ou'll have far few er problem s w ith reso u rce leaks. T h e CCom Ptr, CCom QIPtr, and CComDi s p a t c h D r i v e r sm art pointer classes ease, but do not totally alleviate, the resource m anagem ent needed for interface pointers. T hese classes have num erous useful m ethods that let you w rite m ore ap­ plication code and deal less w ith th e low -level resource m anagem ent details. Y ou’ll find sm art pointers m ost useful w hen you’re using interface pointers w ith code that can throw exceptions. CHAPTER 3 Objects in ATL A T L ,s fundam ental support for CO M can be split into tw o pieces: objects and servers. T his chapter covers classes and concentrates on how I U n k n o w n i s i m ­ plem ented as related to threading and various CO M identity issues, for exam ple, standalone versus aggregated objects. T he next chapter focuses on how to expose classes from CO M servers. Recall: COM Apartments CO M supports tw o kinds of objects, m ultithreaded and single threaded. M ulti­ threaded objects are those th at perform their ow n synchronization, w hereas single­ threaded objects are objects that prefer to let CO M handle the synchronization. Single-threaded objects are easier to im plem ent, although m ultithreaded objects m ay have a higher degree of concurrency. A n object is know n to CO M as single threaded or m ultithreaded based on the apartm ent in w hich it lives. A CO M apartment is a group of one or m ore threads m arked as either single threaded or m ultithreaded. A thread is m arked w ith a call toC oInitializeE x: HRESULT ColnitializeEx(void* pvReserved, DWORD dwCoInit); The dw CoInit param eter is either COINIT_APARTMENTTHREADED or CO IN IT_ MULTITHREADED, based on w hether objects created on th at thread are to be single threaded or m ultithreaded, respectively. C alls to C olnitialize m ap to a call t o Colni tializeE x, p a s s i n g COINIT^APARTMENTTHREADED. By calling C o l n i - ti a l i zeE x, a th read is said to be joining an apartm ent In the case of a single~threaded ap artm en t (STA ), C O M creates an invisible w in­ dow to queue requests for objects in that apartm ent. T o service these requests, the thread m ust pum p a W indow s m essage loop. For exam ple: int WINAPI WinMain(HINSTANCE. HINSTANCE, LPSTR, int) { ColnitializeEx(0, COINITJ^PARTNENTTHREADED); // or CoInitialize(O); 93 ATL INTERNALS MSG msg; whileC CetMessage(&msg, 0, 0, 0) ) D i spatchMessage(&msg); CoUni nitializeO; } S ince the req u ests can only be serviced one at a tim e, each call to an ob ject created in a single-threaded apartm ent m ust com plete before another can be serviced. T his is how CO M provides synchronization for single-threaded objects. O n the other hand, objects created in a m ultithreaded apartm ent (M TA ) m ust be prepared to receive calls from any num ber of threads sim ultaneously. M ulti­ threaded objects are responsible for synchronizing th eir ow n state. In-process Server Considerations A lthough clients and executable servers m anage all t h e i r ow n threads, and there­ fore are responsible for creating their ow n apartm ents, in-process servers have no such luxury. W hen an in-process serv er is first loaded, it w ill be in the co n tex t of a th read th at has already joined an apartm ent. A ny th read th at calls CoG etCl ass- Obj ect or C oC reatelnstance m ust already have jo in ed an apartm ent, or the call w ill fail im m ediately.1 So, does th at m ean th at all objects exposed from in-process servers m ust be equally at hom e in single-threaded and m ultithreaded apartm ents so th at random clients can’t cause harm ? F ortunately, no. W hen, in the process of accessing a class object, CO M notices th at the thread­ ing m odel of the calling apartm ent is incom patible w ith that of the class being activ ated , it w i l l call th e class o bject from th e co n tex t o f another, com patible, ap art­ m e n t , even creating a new apartm ent if necessary. To allow com m unication be­ tw een any tw o apartm ents w hile m aintaining the synchronization and concurrency requirem ents of both apartm ents, CO M w ill load proxy/stubs on dem and, that is, w hen an interface pointer is passed betw een tw o apartm ents. T o notify CO M to provide the appropriate protection, a class w ill be m arked in the registry using th e T hreadi ngM odel nam ed value under the InProcServerB 2 key. U sing th e . 「 e g file form at, an ap artm en t m odel in-process class w ould m ark it­ self thusly: RECEDIT4 [HKEY_CLASSES_ROOT\CLSID\{44EBF751-...}\InProcServer32] @=bworsvr.dll ThreadingModel=Apartment 1 COM requires that Colni t i al i zeEx be called before any “interesting1* COM calls are allowed. OBJECTS IN ATL T h e T hreadi ngM odel nam ed value has the follow ing possible values. _ ( n o n e ): If th ere is n o T hreadi ngM odel given in th e registry, C O M w ill assum e a legacy single-threaded class and access the class object from the process’s first STA , creating a new STA if necessary. T his p ro tects objects from having to synchronize access to global, static, or instance data. ■ Apartment: T his protects objects from having to synchronize access to in­ stance data; how ever, objects of this class could be created in different STA s, so static and global d ata m ust still be synchronized. ■ F r e e : T his value ind icates th at th e objects of th is class w ould like to live only in the M TA. O bjects living in the M TA m ust synchronize access to instance as w ell as static an d global data. ■ B o t h : O bjects that w ant to share the sam e apartm ent as the client w ill m ark th eir class as B o t h , w hich m eans that th ere is no incom patible client threading m odel. T his value is used w hen you w ant to avoid the overhead of a proxy/stub pair betw een th e initial client and the object. Implementing IUnknown A COM object has one responsibility: im plem enting the m ethods of I U n k n o w n . T hose m ethods perform tw o services, lifetim e m anagem ent and runtim e type dis­ covery. as follow s: interface IUnknown { // runtime type discovery HRESULT QuerylnterfaceC[in] REFIID riid, [out, iid_is(riid)] void **ppv); // lifetime management ULONG AddRef(); ULONC ReleaseO ; } COM allow s every object to im plem ent these m ethods as it chooses (w ithin certain restrictions, as described in C hapter 5). T he canonical im plem entation is a s f o l l o w s : // Server lifetime management extern void ServerLockO; extern void ServerUnlockO; L INTERNALS class CPenguin : public IBird, public ISnappyDresser { public: CPenguin() : m一 cRef(O) { ServerLockC); } virtual 〜CPenguinO { ServerUn1ock() ; } // IUnknown methods STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if( riid == IID_IBird || riid == IID一IUnknown ) ☆ppv = stati c_cast(thi s); else if( riid == IID.ISnappyDresser ) *ppv = static_cast(thi s); else *ppv = 0; if( *ppv ) { reinterpret一cast(*ppv)->AddRef(); return S一 OK; } return E.NOINTERFACE; > STDMETHODIMP_(ULONC) AddRef() { return InterlockedIncrement(reinterpret_cast(&m_cRef)); } STDMETHOOIMP_(ULONC) Release() ULONG 1 = Inter1ockedDecrement(reinterpret_cast(&m_cRef)); if( 1 *= 0 ) delete this; return 1; } II IBi rd and ISnappyDresser methods… private: ULONC m_cRef; T his im plem entation of I U n k n o w n is based on several assum ptions : _ T he object is heap based because it rem oves itself using th e d e l e t e operator. F urther, the object’s lifetim e is com pletely governed by its outstanding refer­ ences. W hen it has no m ore references, it deletes itself. OBJECTS IN ATL ■ T he object is able to live in a m ultithreaded apartm ent because it m anipulates the reference count in a thread-safe m anner. O f course, the other m ethods m ust b e im plem ented in a thread-safe m anner as w ell for the object to be fully thread safe. ■ T he object is standalone and cannot be aggregated because it does not cache a reference to a controlling outer nor does it forw ard the m ethods of I U n k n o w n to a controlling outer. ■ T he object exposes its interfaces using m ultiple inheritance. ■ T he existence of tho object keeps the server running. T he constructor and the destructor are used to lock and unlock the server ,respectively. W hile these are com m on assum ptions, they are not the only possibilities. C om ­ m on variations include the follow ing: ■ A n object m ay be global and live for the life of the server. Such objects have no need of a reference count because they never delete them selves. ■ A n object m ay not need to be thread safe because it m ay only be m eant to live in single-threaded apartm ents. ■ A n object m ay choose to alJow itself to b e aggregated as w ell as, o r instead of, supporting standalone activation. ■ A n object m ay expose interfaces using other techniques besides m ultiple in­ heritance, including n ested com position, tear-offs, and aggregation. ■ Y ou m ay not w an t the existence of an o b ject to force the serv er to keep running. T his is com m on for global objects, because their m ere existence w ould prohibit the server from unloading. C hanging any of these assum ptions w ill result in a different im plem entation of I U n k n o w n , although the rest of the object’s im plem entation is unlikely to change m uch (w ith the notable exception of thread safety). B ee 汪use these im plem entation details of I U n k n o w n , w hile im portant, tend to take a very regular form , they can be encapsulated into C +十 classes. Frankly, w e’d really like to use som eone else's tested code and to be able to change our m inds later w ithout a great deal of effo rt W e’d also like this boilerplate code to be easily separable from the actual behavior of our objects so that w e can focus on our dom ain-specific im plem entation. ATL w as designed from the ground up to provide just this kind of functionality and flex ib ility . ATL INTERNALS (• CComObjectRootBase , -----------— CpComObjectRoctEx jr" , _______一 〜 、 一 ••-------—一 、 ( IXxxImpl ) — - 平............ 一 ( IBird ) nappy Dresser〉 CVourClass The Layers of ATL A TL’s support for building CO M objects is separated into several layers, as show n i n F i g u r e 3 . 1 . T hese layers break dow n into services exposed by ATL for building o b j e c t s . 1 . A CComXxxThreadM odel is used by CCom ObjectRootEx to provide just- thread-safe-enough object lifetim e m anagem ent and object locking. 2 . CCom ObjectRootBase and CComObjectRootEx provide helper functions used in im plem enting IUnknown. 3. Y our class derives from CCom O bj e c t R o o t E x . Y our class m ust also derive from any interfaces it w ants to im plem ent as w ell as providing the m ethod im ple­ m entations. M ethod im plem entations m ay be provided by you or by one of the A T L I X x x I m p l c l a s s e s . 4 . C C o m O b j e c t et al. provide the actual im plem entation of the m ethods of I U n k n o w n in a w ay consistent w ith your desires fo r object and server lifetim e m anagem ent requirem ents. T his final layer w ill actually derive from your class. Y our choice of base classes and the m ost derived class w ill determ ine the w ay in w hich the m ethods of I U n k n o w n are im plem ented. Should your choices change, OBJECTS IN ATL using different classes at com pile tim e (or runtim e) w ill change how A TL im ple­ m e n t s I U n k n o w n , independently of the rest of the behavior of y our object. T he fol­ low ing sections explore each of the layers of A T L Threading Model Support Just Enough Thread Safety T he thread-safe im plem entation of A ddR ef and Rel ease show n previously m ay be overkill fo r o u r C O M objects. F or exam ple, if in stan ces o f a specific class w ill only live in a single-threaded apartm ent, there's no reason to use the thread-safe W in32 f u n c t i o n s Inter!ockedlncrem ent and InterlockedD ecrem ent. F or single- threaded objects, the follow ing im plem entation of AddRef and R elease i s m o r e e f f i c i e n t : class Penguin { • • ■ ULONG AddRefO { return —nucRef; } ULONC ReleaseO { ULONC 1 = —m_cRef; if( 1 =* 0 ) delete this; return 1; A lthough using the thread-safe W in32 functions w ill also w ork for single^ threaded objects, unnecessary thread safety requires extra overhead that w e’d like to avoid if w e can. F or this reason, ATL provides three classes, C C o m S i n - gl eTh readM odel, CComMul ti Th readM odel,and CCom M ul ti T h readM odel NoCS. T hese classes provide tw o static m em ber functions, Increm ent and D ecrem ent, for abstracting aw ay the differences betw een m anaging an object's lifetim e co u n t in a m ultithreaded o r single-threaded m anner. T he tw o versions of these functions are as follow s (notice that both CComMul tiT hreadM odel and CComMul ti T hread- M o d e l N o C S have identical im plem entations of these functions). class CComSi ngl eTh readModel { static ULONC WINAPI Inc resent (LPLONC p) { return ♦♦(»; } 100 ATL INTERNALS static ULONC WINAPI Oecrement(LPLONC p) { return — (*p); } class CComMultiThreadModel { static ULONG WINAPI IncreiientCLPLONC p) { return Interlockedlncrement(p); } static ULONC WINAPI Decrement(LPLONC p) { return InterlockedOecrement(p); } class CComMu11iThreadMode1NoCS { static ULONC WINAPI Increment(LPLONG p) { return Inter!ockedlncrement(p); } static ULONC WINAPI Oecresent(LPLONC p) { return InterlockedDecrement(p); } U sing these classes,w e can param eterize2 our class to give a just-thread-safe- e n o u g h A ddR ef and Rel ease im plem entation, like so : template c l a s s Penguin { « 參• ULONC AddRefO { return ThreadModel::Incresent(&n_cRef); } ULONC Release() { ULONC 1 * ThreadModel: :Decrea«ntClflL.cRef); if( 1 =» 0 ) delete this; return 1; } 2 For an introduction to C + + templates and how they’re used m ATL, see Appendix A. OBJECTS IN ATL Now, based on our requirements for the CPengui n class, we can make it just thread- safe enough by supplying the threading model class as a template parameter. II Let's make a thread-safe CPenguin CPenguin* pobj = new CPengui n; Instance Data Synchronization When you create a thread-safe object, protecting the object’s reference count isn’t enough. We’ll also have to protect the member data from multithreaded access. One popular method for protecting data accessible from multiple threads is to use a Win32 critical section object, as shown here: template class CPenguin { p u b lic : CPenguinO: m_nWingspan(0) { ServerLockC); Ini tia li zeCriti calSection(&m_cs); } -CPenguin() { Serve「Uniock(); DeleteCriticalSection(&m_cs); } / / IB ird STDMETHOOIMP get_Wi ngspan(1ong* pnWingspan) { L o c k (); II Lock out other threads during data write ★pnWingSpan = m_nWi ngspan; U nlockC ); re tu rn S-OK; } STDMETHOOIMP put_Wi ngspan(1ong nWingspan) { Lock(); // Lock out other threads during data read m_nWingspan = nWingspan; U n lo c k O ; re tu rn S_OK; } • • t p riv a te : long m_nWingspan; CRITICAL_SECTION m_cs; void LockO { EnterCriti cal Secti on(&m_cs); } void UnlockC) { LeaveCriticalSectionC&m_cs); } 102 ATL INTERNALS Notice that before reading or writing any member data, the CPengui n object enters the critical section, locking out access by other threads. This coarse-grained, object- level locking keeps the scheduler from swapping in another thread that may corrupt the data members during a read or a write on the original thread. However, object-level locking doesn’t give us as much concurrency as we may like. If we have only one critical section per object, one thread may be blocked try­ ing to increment the reference count while another is updating an unrelated mem­ ber variable. A greater degree of concurrency would require more critical sections, allowing one thread to access one data member while a second thread accessed an­ other. Be careful using this kind of finer-grained synchronization, however, as it of­ ten leads to deadlock. For example: cla ss CSneech : p u b lic ISneech { p u b lic : / / ISneech STDMETHODIMP GoNorth() { EnterCriticalSection(&m_csl); // Enter c s l... EnterCriticalSectionC&m_cs2); // . . .then enter cs2 II Go north... LeaveCriticalSection(&m_cs2); LeaveCri t i cal Secti on(&fr_csl); STDMETHODIMP GoSouth() { EnterCritical Secti on C&tii_cs2) ; / / Enter cs2... EnterCriticalSectionC&«_csl); // . . .then enter csl // Go south... LeaveCri t i cal Secti on (&ni_csl); LeaveCri t i calSecti on(&m_cs2); p riv a te : CRITICAL一SECTION m _csl; CRITICAL_SECTION m_cs2; Imagine that the scheduler lets a thread executing the GoNorth method enter the first critical section and then swaps in a thread executing the GoSouth method that enters the second critical section. If this happens, neither thread can enter the other OBJECTS IN ATL 103 critical section and therefore neither thread w ill be able to proceed, leaving them deadlocked. This situation should be avoided.3 Whether you decide to use object-level locking or finer-grained locking, critical sections are handy. ATL provides two class wrappers that simplify their use: CCom- C ri t i cal Secti on and CComAutoCri t i cal Secti on. class CCowCriticalSection { p u b lic : void Lock() { EnterCriticalSection(Am^sec); } void UnlockO { LeaveCriti cal Secti on C4m_sec); > void Ini t() { Ini t i al i zeCri t i cal Secti on (i#n«sec) void TermO { Del eteC riti cal Secti on (Am_sec); } CRITICA匕 SECTION m.sec; class CCoaAutoCriticalSection p u b lic : void Lock() { void UnlockO { CComAutoCrit1calSection() { -CComAutoCriticalSection() { CRITICAL_SEaiON m_sec; EnterCriticalSection(Am 一s e c ); } LeaveCri t i calSecti on(Am_sec); } Ini tializeCriticalSection(Anusec) DeleteCriticalSection(Anusec); } Notice that CComCri t i cal Secti on has no constructor or destructor, but rather has In i t and Term functions. This makes CComCri t i cal S ecti on useful when a global or static critical section is required and the C runtime library (CRT) is not available to perform automatic construction and destruction. CComAuto­ C ri t i cal S ecti on, on the other hand, is easier to use, both in the presence erf the CRT as well as when created as an instance data member. For example, using a CComAutoCri t i cal S ecti on in our CPengui n class simplifies the code a bit: template class CPenguin { p u b lic : // IBird methods call Lock() and UnlockO as before... 3 For quite a bit more guidance on what to do about deadlocks, read Win32 Multithreaded Program­ ming, by Mike Woodring and Aaron Cohen (O'Reilly & Associates, 1997). 104 ATL INTERNALS p riv a te : CCooAutoCrl t ic a l Section hl.cs; void Lock() { »_cs.LockO; } void UnlockO { ■.cs.linlockQ; > However, notice that our CPengui n is still parameterized by the threading model. There’s no sense in protecting our member variables in the single-threaded case. In this case, it would be handy to have another critica l section class that could be used in place of CComCri t i cal S ecti on or CComAutoCri t i cal S ecti on. ATL provides the CComFakeCri t i cal S ecti on class for this purpose. class CComFakeCriticalSection { p u b lic : vo id Lock() {} vo id Unlock(> {> vo id In itO O vo id }: Term() {> Given CComFakeCri t i cal S ecti on, we could further parameterize the CPen­ gui n class by adding another template parameter, but this is unnecessary. The ATL threading model classes already contain type definitions that map to a real or a fake critical section, based on whether we’re doing single or multithreading: class CComSingleThreadModel { p u b lic : s ta tic ULONC WINAPI Xncrement(LPLONC p) { re tu rn + ^ (*p ); > s ta tic ULONC WINAPI Decrement(LPLONC p) { re tu rn — (* p )| } it^pedef CCosFakcCrlticalSection AutoCrit1calS«ct1oii; typ«d«f CCcMFakeCr1t1ca1 S«ct1on C ritic a lS c c tio n ; typcdef CComSi ngleThreadHodel ThreadModelNoCS; class CComMulti ThreadModel { p u b !ic : s ta tic ULONC WINAPI Increment(LPLONC p) { return Interlockedlncrement(p); } OBJECTS IN ATL s t a t ic ULONC WINAPI Decrement(LPLONC p) { return InterlockedOecrement(p); > typ«def CComAutoCriticalSection AutoCri1 1calS«ction; typ«def CComCriticalSection CriticalSection; typedef CComMultiThreadModelNoCS ThreadModelNoCS; class CComMultiThreadModelNoCS { p u b lic : s ta t ic ULONC WINAPI Increment(LPLONC p) { return Inter!ockedlncrement(p); } s t a t ic ULONC WINAPI Decrement(LPLONC p) { return InterlockedDecrement(p); } typedef CCosFakcCr1tlc a lS e c tio n A u to C ritle a lS e ctio n ; typ cd cf CCo«FakcCr1t1ca1S«ction Cr1t1ca1S«ct1o«i;:1ca1Sec •eadModetyp e d e f CComMulti ThreadModelNoCS ThreadModelNoCS Using these type definitions allows us to make the CPengui n class just thread-safe enough both for the object’s reference count and for coarse-grained object syn­ chronization: template class CPenguin { p u b lic : // IBird methods as before," • • • p riv a te : ThrcadlngModcl: :AutoCriticalSection «.cs; void Lock() { m_cs.Lock(); } void UnlockC) { m_cs.UnlockO; } This technique allows us to provide the compiler with operations that are just thread-safe enough. When the threading model is CComSi ngl eThreadModel, the calls to Increment and Decrement resolve to operator++ and operator ~ , and the Lock and Unlock calls resolve to empty inline functions. When the threading model is CComMul tiThreadModel, the calls to In ­ crement and Decrement resolve to calls to Inter!ockedlncrem ent and 106 ATL INTERNALS Table 3.1. Expanded Code Based on Threading Model Class CComSingle- ThreadModel CComMultiThreadModel CComMulti- ThreadModelNoCS TM::Increment + + Interlockedlncrement Interlockedlncrement TM::Decrement InterlockedDecrement InterlockedDecrement TM::AutoCri t i calSecti on:: Lock (n o th in g ) EnterCriticalSection (n o th in g ) TM::AutoCriticalSection:: Unlock (n o th in g ) LeaveCriticalSection (nothing) InterlockedDecrement, and the Lock and Unlock calls resolve to calls to EnterCri t i cal Secti on and LeaveCri t i cal Secti on. Finally, when the model is CComMul tiThreadM odel NoCS, the calls to In c re ­ ment and Decrement are thread safe, but the critical section is fake, just like CComSi ngl eThreadModel. CComMul tiThreadM odel NoCS is designed for multi­ threaded objects that eschew object-level locking in favor of a more fine-grained scheme. Table 3.1 shows how the code is expanded based on the threading model class you use. The Server’s Default Threading Model ATL-based servers have a concept of what is the server's “default’, threading model. This is used for things that you don’t specify directly. To set the server’s default threading model, you define one of the following symbols: _ATL_SINGLE_ THREADED, _JVTL_APARTMENT_THREADED, o r ^ATL_FREE_THREADED. I f you don’t specify one of these symbols, ATL w ill assume _AT L_F R E E_THR EADED. However, the ATL COM AppWizard w ill define _jVI*L^APARTMENT_THREADED in the generated s td a fx . h file. ATL uses these symbols to define two type definitions: # i f d e fi ned (_ATLwSINCLE_'mREADEO) typedef CComSi ngleTh readModel CComObjectThreadModel; typedef CComSi ngleThreadModel CComClobalsThreadModel; # e l i f definedCJVTLJVPARTMENT.THREADED) typedef CComSi ngleThreadModel CComObjectThreadModel; typedef CComMultiThreadModel CComClobalsThreadModel; #else / / J\TL_FREE_THREADED typedef CComMultiThreadModel CComObjectThreadModel; typedef CComMultiThreadModel CComClobalsThreadModel; # e n d if OBJECTS IN ATL Internally, ATL uses CComObjectThreadModel to protect instance data and CComCl obal sTh readModel to protect global and static data. Because the usage is difficult to override in some cases, you should make sure that ATL is compiled us­ ing the most protective threading model of any of the classes in your server. In prac­ tice, this means you should change the wizard-generated _ATIAPARTMENT 一 THREADED symbol to _ATL_FREE_THREADED if you have even one multithreaded class in your server. The Core of IUnknown Standalone Reference Counting To encapsulate the Lock and Unlock methods as well as the just thread-safe enough reference counting, ATL provides the CComOb j ectRootEx base class, pa­ rameterized by the desired threading model.4 template class CCoiRObjectRootEx : public CCowObjectRootBase { p u b lic : typedef ThreadModel ^ThreadModel; typedef ^ThreadModel: :AutoCriticalSection 一C ritS e c ; typedef CComObjectLockT<_ThreadModel> ObjectLock; ULONC InternalAddRef() { ATLASSERT(«L.dwRef ! • -1 L ); return ..ThreadModel: :Increment(Am_dwRef); ULONC InternalRelease() { ATLASSERT(m^dwRef > 0 ); return _ThreadModel: : DecrementC&m^dwRef); } void Lock() { m_critsec.Lock(); } void UnlockO { m_cr1tsec.UnlockC); } p riv a te : •CritSec m_critsec; 4As an optimization, ATL provides a specialization of CComObjectRootEx that does not have a _Cri tSec object, sidestepping minimum object size requirements imposed by the compiler. 108 ATL INTERNALS ATL classes derive from CComObj ectRootEx and forward AddRef and Release calls to the InternalAddR ef and InternalR el ease methods when the object is created standalone (that is, not aggregated). With the Lock and Unlock methods so readily available in the base class, we may be tempted to write the following incorrect code: class CPenguin : public CComObjectRootEx, ... { STDMETHODIMP get_Wingspan(1ong* pnWingspan) { LockQ ; i f ( 丨pnWingspan ) re tu rn E-POINTER; / / Forgot to Unlock ♦pnWingspan = m_nWingspan; UnlockO ; re tu rn S_0K; To help us avoid this kind of mistake, CComObj ectRootEx provides a type defini­ tion for a class called Ob j ectLock, based on CComObj ectLockT parameterized by the threading model, as shown: template c la s s CCoaiOb j ec t LockT { p u b lic : CComObjectLockT(CCom0bjectRootEx* p) { i f (p ) p->Lock(); m_p « p; } ^CComObj ectLockTO { i f Cnu>) m _p->U nlock(); CComObjectRootEx^hreadMode^1* m_p; Instances of CComObj ectLockT w ill Lock the object passed to the constructor and Unlock it upon destruction. The O bject Lock type definition provides a con­ venient way to write code that w ill properly release the lock regardless of the re­ turn path: Class CPenguin : public CComObj ectRootEx, ... { STDMETHOOIMP getJWingspan(long* pnWingspan) { OBJECTS IN ATL ObjectLock lock(this); if( !pnWingspan ) return E_POINTER; / / Stack unwind invokes Unlock *pnWi ngSpan = m_nWingspan; re tu rn S_0K; Table^Driven Query Interface In addition to just-thread-safe-enough implementations of AddRef and Rel ease for standalone COM objects, CComObj ectRootEx, via its base class, CComObj e c t- RootBase, provides a static, table-driven implementation of Q uerylnterface called Internal Querylnterface. s ta tic HRESULT WINAPI CCofnObj ect Root Base:: Interna1Q ueryInterface( void* pThis, const JVTL.INTMAP.ENTRY^ p E n trie s . REFIIO iid, v o id *。 ppvObject); This static member function uses the th i s pointer of the object, provided as the pThi s parameter, and the requested interface to fill the ppvOb je c t parameter with a pointer to the appropriate virtual function table pointer (vptr). It does this using the pE ntries parameter, a zeroterminated array of _ATL_INTMAP 一 ENTRY structures: S tru c t JVTL_INTMAP_ENTRY { const 110* piid; DWORD dw; _ArL_CRtATORARCFUNC* pFunc; Each interface exposed from a COM object w ill be one entry in the interface map, which is a class-static array of _ATIINTMAP—ENTRY structures. Each entry w ill consist of an interface identifier (IID), a function pointer, and an argument for the function represented as a DWORD. This provides a flexible, extensible mecha­ nism for implementing Q uerylnterface that supports multiple inheritance, ag­ gregation, tear-offs, nested composition, debugging, chaining, and just about any 110 ATL INTERNALS CPenguin IBird vtbl IBird vptr ', <. — Querylnterface ISnappyDresser vptr 讀 AddRef m_dwRef \ Release \ Fly ISnappyDresser vtbl Querylnterface AddRef Release DressSnappily Figure 3.2. CPenguin object layout including vptxs to vtbls other wacky COM identity tricks currently in use by C+ + programmers.5 However,)gramm( itance, >since most interfaces are implemented using multiple inheritance, we don’t often need this much flexibility. For example, consider one possible object layout for in­ stances of the CPengui n class, as shown in Figure 3.2. class CPenguin : public IBird, public ISnappyDresser {•••}; The typical implementation of Q uerylnterface for a class using multiple in­ heritance consists of a series of i f statements and s ta ti c_cast operations, the purpose of which is to adjust the th i s pointer by some fixed offset to point to the appropriate vptr. Because the offsets are known at compile time, a table that matched interface identifiers to offsets would provide an appropriate data structure for adjusting the th i s pointer at runtime. To support this common case, the In ­ ternal Q uerylnterface function treals the __ATUMTMAP 一 ENTRY as a simple IID/ofEiset pair if the pFunc member has the special value _ATISIMPLEMAPENTRY: #define _ATL_SIMPLEMAPENTRY ( CATL.CREATORARCFUNC*) 1) To be able to use the In te rn a l Q uerylnterface function, each implementa­ tion populates a static interface map. To facilitate populating this data structure, 8 All these uses of the interface map are described in Chapter 5. OBJECTS IN ATL 111 and to provide some other methods used internally, ATL provides the following macros (as well as others described in Chapter 5): #define BECIN.COM^AP(class) ... #define C0PUINTERFACE^ENTRY(itO … #define END_COH.MAP() . . . For example, our CPengui n class would declare its interface map like so: class CPenguin : public CComObjectRootEx, public IBird, public ISnappyDresser { ■ • • p u b lic : IUnknown* GetUnknownO { ATLASSERTCCetEntriesO [0 ] .pFunc == JMU-SIMPLEMAPENTRY); return (IUnknown*) CC1nt) th i s+_CetEntricsO->dw); > > HRESULT ^.InternalQuerylnterface(REFIID iid , void** ppvObject) { return InternalQuerylntsrfaceCthis, ^CetEntriesQ, iid , ppvObject); ENTRY* WINAPI _G e tE n trie s() { P_ENTRY _ e n trie s [] = { 0, JMU_SIHPLEMAPENTRY >, , 4 , J\TU_SIHPLEMAPENTRY } , { &IID.IBird, { &IID.ISnappyOresser 112 ATL INTERNALS return .entries; Figure 3.3 shows how this interface map relates to an instance of a CPenguin ob­ ject in memory. Something else worth inentioiilng is the GetUn known member function that the BEGIN一 COM_MAP provides. Although this is used internally by ATL, it’s also useful when passing your th i s pointer to a function that requires an IUnknown*. Because your class derives from potentially more than one interface, each of which derives from IUnknown, passing your own th i s pointer as an IUnknown* is considered ambiguous by the compiler. For example: HRESULT FlylnAnAi rplane(IUnknown* punkPassenger); II Penguin.cpp STDMETHODIMP C P e n g u in ::F ly () { return FlylnAnAirplane(this); // ambiguous CPenguin CPenguin piid &IID_IBird IBird vptr f dw 0 CetUnknown()); / / unambiguous } As we’ll see later, GetUn known is implemented by handing out the first entry in the interface map. Support for Aggregation: The Controlled Inner So far, we’ve discussed the implementation of IUnknown for standalone COM ob­ jects. However, when our object is to participate in aggregation as a controlled in­ ner, our job is not to think for ourselves but rather to be subsumed by the thoughts and desires of another. A controlled inner does this by blindly forwarding all calls on the publicly available implementation of IUnknown to the controlling outer’s im­ plementation. The controlling outer’s implementation is provided as the pUnk- Outer argument to the Createlnstance method of ICIassFactory. When our ATL-based COM object is used as a controlled inner, it simply forwards all calls to IUnknown methods to the OuterQuerylnterface, OuterAddRef, and Outer- Release functions provided in CComObjectRootBase, which, in turn, forward to the controlling outer. The relevant functions of CComObj ect RootBase are shown here. class CCo«ObjectRootBasc { p u b lic : CComObjectRootBaseO { nudwRef « 01; } • • • ULONC OuterAddRefO { return «^)OuterUnknown>>AddR«f(); > ULONC 0uterRe1eas«O { return RLpOuterUnknown_>Re1«aseO; } HRESULT OuterQuerylnterfaccCREFIIO iid , void ** ppvObject) { return m^>OuterUnkno»m->QueryInterface(iidp ppvObject); } union { long nudwRef; 114 ATL INTERNALS IUnknown* m一pOute「Unknown; Notice that CComObj ectRootBase keeps the object’s reference count and a pointer to a controlling unknown as a union. This implies that an object can either maintain its own reference count or be aggregated, but not both at the same time. This implication is not true. When the object is being aggregated, it must maintain a reference count and a pointer to a controlling unknown. In this case, as I’ll discuss later, ATL keeps the m_pUnkOuter in one instance of the CComObjectBase and then derives from CComObj ectBase again to keep the object’s reference count. While it’s possible to implement the methods of IUnknown directly in your class using the methods of the base class CComObj ectRootEx, most ATL classes don’t Instead, the actual implementations of the IUnknown methods w ill be left to 往 class that derives from your class, for example, CComObj ect. I’ll discuss this after I’ve talked about the responsibilities of your class. Your Class Because ATL provides the behavior for IUnknown in the CComObj ectRootEx class and w ill provide the actual implementation in the CComObj e ct (and friends) class, the job your class performs is pretty simple: derive from interfaces and implement their methods. Besides making sure that the interface map lists all the interfaces you’re implementing, you can pretty much leave implementing IUnknown to ATL and concentrate on your custom functionality. This is, after all, the whole point of ATL in the first place. ATL's Implementation Classes • Many standard interfaces have common implementations. ATL provides plementation classes of many standard interfaces; for example, Stream lnitlm pl, IConnectionPointContai nerlmpl, and IViewObjectEx- Impl implement IP ersi stStream lni t, IConnecti onPoi ntContai ner, and I Vi ewObjectEx, respectively. Some of these interfaces are common enough that many objects may implement them, for example, persistence, eventing, or enumer­ ation. Some are more special purpose and related only to 压 particular framework, for example, controls, Internet-Enabled components,or Microsoft Management Console extensions. Most of the general-puipose interface implementations w ill be discussed in Chs^ters 6,7, and 8. The interface implementations related to the con­ trols framework w ill be discussed in Chapters 10 and 11. One implementation is general purpose enough to discuss right here: ID i spatchlm pl. OBJECTS IN ATL 115 Scripting Support For a scripting environment to access functionality from a COM object, the COM object must implement ID i spatch, as shown here. interface IDi spatch : IUnknown { HRESULT C€tTyp«InfoCountC[out] UINT * petinfo) HRESULT C € tT y p e In fo ([in ] UINT iT In fo , ( in ] LCID lc id , [out] ITypelnfo ** ppTInfo); HRESULT C€tIDsOfNa»es([in] [ in . [ in ] [ in ] [o u t REFIID M id. size_is(cNames)] LPOLESTR • rgszNames UINT cNames, LCID lc id , size_is(cNan»es)] DISPID * rgDi spld); HRESULT In v o k e ([in ] [in ] [ in ] [ in ] dispIdMember r i i d , DISPID REFIIO LCID lc id , WORD wFlags, [ in , o u t] OISPPARAMS * pOispParams [out] VARIANT * pVarResult, [out] EXCEPINFO * pExcepInfo, [o u t] UINT * puArgErr); The most important methods of ID i spatch are GetlDsOfNames and Invoke. Imagine the following line of scripting code: penguin.wingspan = 102 This w ill translate into two calls on ID i spatch. The first w ill be CetlDsOfNames, which w ill ask the object if it supports the wi ngspan property. When the answer is yes, the second call to ID i spatch will be to Invoke. This call will include an identifier (called a DISPID) that uniquely identifies the name of the property or method the client is interested in (as retrieved from CetlDsOfNames), the type of operation to perform (calling a method or getting or setting a property), a list of arguments, and a place to put the result (if any). The object’s implementation of Invoke must then interpret the request made by the scripting client This typically 116 ATL INTERNALS involves unpacking the list o f arguments (w hich is passed as an array of VARIANT structures), converting them to the appropriate types (if possible), pushing them onto the stack, and calling some other implemented method that deals in real data types, not VARIANTS. In theory, the object's implementation could take any number of interesting, dy­ namic steps to parse and interpret the client’s request. In practice, most objects for­ ward the request to a helper, whose job it is to build a stack and call a method on an interface implemented by the object to do the real work. The helper makes use of type information held in a type library typically bundled with the server. COM type libraries hold just enough information to allow an instance of a Typelnfo object (that is, an object that implements IT ypelnfo ) to perform this service. The Type­ lnfo object used to implement ID i spatch is usually based on a dual interface, defined in IDL like so: [ d u a l, uuid(44EBF74E-116D-llD2-9828-00600823CFFB)] interface IPenguin : IDispatch { [propput] HRESULT Wingspan([in] long nWingspan); [propget] HRESULT Wingspan([out, retval] long* pnWingspan); Using a Typelnfo object as a helper allows an object to implement ID i spatch like this (code m bold indicates differences between one implementation and another): class CPenguin : p u b li c CComObjectRootEx, public IBird, public ISnappyDresser, public IPenguin { p u b lic : CPengui n() : m__pTypeInfo(0) { IID* plID = «rIIO_XPenguin; CUID* IBID.BIRDSERVERLib; WORD wMinor = 0 ; ITypeLib* ptl =0; HRESULT hr = LoadRegTypeLib(*pLIBIDf wMajor, wMinor, 0 , A p tl) ; i f ( SUCCEEDEDChr) ) { hr = ptl->GetTypeInfoOfGuid(ApIID , &m_pTypeInfo); HRESULT F ly Q ; ptl->Release(); OBJECTS IN ATL 117 v ir t u a l 〜CPenguin() { ifC m_pTypeInfo ) m_pTypeInfo->ReleaseC); BECIN_COM^MAP(CPenguin) COM_INTERFACE^ENTRY(IBird) COM_INTERFACE_ENTRYCISnappyOresser) COM_INTERFACE_ENTRYCIDispatch) COM_INTERFACE_ENTRY(IPenguin) END一C0M_MAP(〕 // IDispatch methods STDMETHODIMP CetTypeInfoCount(UINT ^pctinfo) { return (*pctinfo = 1), S_OK; STDMETHODIMP CetTypelnfoCUINT ctinfo, LCID lc id , ITypelnfo **ppti) { i f ( c tin fo ! * 0 〕 re tu rn ( * p p ti » 0 ), DISP_E_BADINOEX; return (*ppti = m_pTypeInfo)->AddRefQ, S一 OK; STDMETHODIMP CetlDsOfNames(REFIID r iid , OLECHAR * * rgszNames, UINT cNames, LCID lc id , DISPID *rgdispid) { return nupTypeInfo->CetIDsOfNames(rgszNames, cNames, rgdispid); STDMETHODIMP Invoke(DISPID dispidMember, REFIID riid , LCID lc id , WORD wFlags, DISPPARAMS *pdispparams, VARIANT ♦ pvarR esult, EXCEPINFO *pe xce p in fo , UINT *puA rgE rr) { return nupTypelnfo->Invoke(static„castCth1s), dispidMember, wFlags, pdispparams( pvarResult, pexcepinfo, puArgErr); } 118 ATL INTERNALS // IBird, ISnappyDresser, and IPenguin methods p riv a te : ITypelnfo* rrupTypelnfo; Since this iinplementation is so boilerplate (it only varies by the dual interface type, the interface identifier, the type library identifier, and the m^jor and minor ver­ sion numbers), it can be easily implemented in a template base class. ATL’s pa- raineterized implementation of ID i spatch is ID i spatchlm pl. tempiate cla ss ATL.NO.VTBL ID isp a tch lm p l : p u b lic T Given ID i s p a tc h lm p l ,our IPengui n implementation gets quite a bit simpler class CPenguin : p u b lic CComObjectRootEx, public IBird, public ISnappyDresser, public ID1spatchlapl { p u b lic : BECIN«C0M_MAP(CPenguin) COH.INTERFACE_ENTRY(IBird) COM.INTERFACE_ENTRY(ISnappyOresser) COK-INTERFAC^ENTRYCIDi spatch) COfLINTERFACE_ENTRY(IPengui n) END_C0M^1APC) / / IB ird, ISnappyDresser and IPenguin methods… Supporting Multiple Dual Interfaces I wish it didn’t, but this question always comes up: “How do I support multiple dual interfaces in my COM objects?” My answer is always, “Why would you want to?” The problem is that, erf the scripting environments Fm familiar with that require an object to implement ID i spatch, not one supports Q uerylnterface. So, while OBJECTS IN ATL it’s possible to use ATL to implement multiple dual interfaces, you have to choose which implementation to hand out as the “default,” that is, the one the client gets when it asks for ID i spatch. For example, let’s say that instead of having a special IPengui n interface that represented the full functionality of my object to scripting clients, I decided to make all the interfaces dual interfaces. [ dual, uuid(...) ] interface IBird : IDi spatch {•••} [ dual( uuid(...) ] interface ISnappyDresser : IDispatch { … }; You may implement both of these dual interfaces using ATL’s ID i spatchlm pl: class CPenguin : public CComObjectRootEx, public IDi spatchlmpl, public IDi spatchlmpl { p u b lic : BECIN_COM_MAP(CPenguin) COM_INTERFACE_ENTRY(IBird) COM一 INTERFACE-ENTRY(ISnappyDresser) Ca^_INTERFACE.ENTRY(IDi spatch) / / anbiguous END_COM_MAP() However, when you fill in the interface map in this way, the compiler gets upset. Remember that the COM_INTERFACE—ENTRY macro essentially boils down to a s ta ti c_cast to the interface in question. Since there are two different interfaces that derive from ID i s p a tc h , the compiler is not able to resolve the one to which you're trying to cast. To resolve this difficulty, ATL provides another macro: fd e fin e C0H.INTERFACE.ENTRY2(i t f , branch) This macro allows us to tell the compiler which branch to follow up the inheritance hierarchy to the ID i s p a tc h base. Using this macro allows us to choose the default ID i spatch interface: class CPenguin : public CComObjectRootEx, public IDi spatchlmpl, public IDi spatchlmpl { p u b lic : ATL 丨 NTERMA’_S BECIN_COM^MAP(CPenguin) COM_INTERFACE>ENTRY(IBird) COM_INTERFACE_ENTRY(ISnappyOresser) C0M_INTERFACE_ENTRY2(IOispatch, IB ird ) / / Compiles (u n fo rtu n a te ly ) END—COM—MAP() Which brings me to my objection. Just because ATL and the compiler conspire to allow this usage doesn’t mean that it’s a good one. There is no good reason to sup­ port multiple dual interfaces on a single implementation. Any client that supports Q uerylnterface w ill not need to use GetlDsOfNames or Invoke. These kinds of clients are perfectly happy using a custom interface as long as it matches their ar­ gument type requirements. On the other hands, scripting clients that don’t support Que ry ln te rfa c e w ill only be able to get to methods and properties on the default dual interface. For example, the following w ill not work: II Since IBird is the default, its operations are available penguin.fly // Since ISnappyDresser is not the default, its operations aren't a v a ila b le penguin.straightenTie / / runtime error So, here’s my advice. Don’t design your reusable, polymorphic COM interfaces as dual interfaces. Instead, if you’re going to support scripting clients, define a single dual interface that exposes the entire functionality of the class, as I did when defining IPengui n in the first place. As an added benefit, this means that you only have to define one interface that supports scripting clients instead of mandating that all of them do. CComObject et al. Consider the following C+ + class: class CPenguin : public CComObjectRootEx, public IBird, public ISnappyDresser { p u b lic : BECIN_COM_MAP(CPenguin) OBJECTS IN ATL COM一 INTERFACE一 ENTRY(IBi rd ) COM_INTERFACE_ENTRV(ISnappyDresser) END_COM^MAP() II IBird and ISnappyOresser methods... / / IUnkno^m Methods not implemented her« Because this class doesn’t implement the methods of IUnknown, the following code w ill fail at compile time: STDMETHODIMP CPenguinCO::CreateInstance(IUnknown* pUnkOuter, REFIID riid f void** ppv) { 參參* CPenguin* pobj * new CPenguin: / / IUnknown not inplenented Given CComObjectRootBase, we can easily implement the methods of IUn­ known. For example: / / Server life tim e management extern void ServerLockC); extern void ServerUnlockC); c la s s CPengu彳n : public CComObjectRootEx, public IBird, public ISnappyDresser { p u b lic : CPenguinC) { ServerLockC); } -CPengu1n() { ServerUnlockC); } BECIN_COM_MAP(CPengui n) COM一 INTERFACE_ENTRY(IBi rd) CON_INTERFACE_ENTRY(ISnappyDresser) END_COM_MAP() II IBird and ISnappyDresser methods... " IUnknown methods for standalone, heap-based objects STDMETHODIMP QuerylnterfaceCREFIID r ild , void** ppv) { return JtnternalQuerylnterfaceCriid, ppv); > 122 ATL INTERNALS STDMETHODIMP_(ULONC) AddRefO { return In ter rial AddRefO; > STDMETHOOIMP_(ULONC) ReleaseO { ULONC 1 * Internal ReleaseO; 1f( 1 == 0 ) delete this; return cRef; Unfortunately, although this implementation does leverage the base class behavior, it has hard-coded assumptions about the lifetime and identity of our objects. For ex­ ample, instances of this class can’t be created as an aggregate. Just as we’re able to encapsulate decisions about thread safety into the base class, we’d like to encapsu­ late decisions about lifetime and identity. However, unlike thread-safety decisions, which are made on a per-class basis and are therefore safe to encode into a base class, lifetime and identity decisions can be made on a per-instance basis. There­ fore, we’ll want to encapsulate lifetime and identity behavior into classes meant to derive from our class. Standalone Activation To encapsulate the standalone, he^vbased object implementation of IUnknown I just showed you, ATL provides CComObject, shown in a slightly abbreviated form here. template class CCoaObject : public Base { p u b lic : typedef Base .BaseClass; | CC ict(void* * NULL) { e.LockC); } // Keeps server loaded -CComObject( ) { m一 dwRef * 1L; Final ReleaseO; ^Hodule.UnlockO; / / Allows server to unload } OBJECTS IN ATL 123 STDMETHOO(QueryInterface)(REFIIO iid , void ** ppvObject) { return _InternalQueryInterface(iid , ppvObject); } STOMETHOO_(ULONC, AddRef)() { return InternalAddRef(); > STOMETHOO_(ULONC, Release)() { ULONG 1 » InternalReleaseC); i f ( l o) d e le te th is ; re tu rn 1; } template HRESULT STDMETHOOCALLTYPE Q u e ryIn te rfa ce (Q ** pp) { return Querylnterface(一 uuidof(Q), (void**)pp); > s ta tic HRESULT WINAPI CreateInstance(CComObject** pp); Notice that CComObject takes a template parameter called Base. This is the base class from which CComObj e c t derives to obtain the functionality of CComOb- j ectRootEx as well as whatever custom functionality we’d like to include in our objects. Given the implementation of CPengui n that did not include the implemen­ tation of the IUnknown methods, the compiler would be happy with CComObject used as follows (although I’ll describe later why new shouldn’t be used directly when creating ATL-based COM objects): STDMETHODIMP CPenguinCO::Createlnstance(IUnknown* pUnkOuter, REFIID riid , void** ppv) { *ppv = 0; i f ( pUnkOuter ) re tu rn CLASS_E_NOAGCRECATION; / / Read on for why not to use new like this! CCo«Object* pobj = new CComObject; if( pobj ) { pobj->AddRef(); HRESULT hr « pobj->QueryInterface(riidf ppv); pobj->ReleaseO; 124 ATL INTERNALS re tu rn h r; } re tu rn 匕 OUTOFMEMORY; } Besides the call to Final Release and the static member function Create- Instance (which are both described in the “ATL Creators" section of this chapter), CComObject provides one additional item of note, the Q uerylnterface member function template.6 template MRESULT STDMETHODCALLTYPE Q ueryIntcrface(Q A* pp) { return QuerylnterfaceC 一 uuidof(Q), (void#r*)pp); } This member function template uses the new capability of the Visual C+ + (VC+ +) compiler to tag a type with a universally unique identifier (UUID). This c^)ability has been available since VC+ + 5.0 and takes the form of a declarative specifier (dec! spec), for example: stru ct _declspec(uuidC>,00000000-0000-0000-C000-000000000046>>»IUnknown These dec! specs are output by the Microsoft IDL compiler and are available for both standard and custom interfaces. You can retrieve the UUID of a type using the _ uui dof operator, allowing the following syntax: void TryToFly(IUnknown* punk) { IBird* pbird =» 0; if ( SUCCEEDED(punk->QueryInterface(—uuidof(pbird)f (void**)&pbird) ){ pbi rd->Fly(); pbird->Release(); } } Using the Q uerylnterface member function template provided in CComObject allows us a bit more syntactic convenience, given a CComOb j ect-based object ref­ erence. For example: 8 For a brief description of member function templates, see Appendix A. OBJECTS IN ATL 125 void TryToFIy(CComObject* pPenguin) { IBird* pbird = 0; if ( SUCCEEDED(pPenguin->QueryInterface(arpbird) ) { pbird->Fly(); pbi rd->Release(); > } Aggregated Activation Notice that the CPengui n class object implementation shown previously dis­ allowed aggregation by checking for a nonzero pUnkOuter and returning CLASS_ E一 NOAGGREGATION. If we want to support aggregation as well as, or instead of, standalone activation, we’re going to need another class to implement the forward­ ing behavior of aggregated instances. For this, ATL provides CComAggOb j ect. CComAggObject performs the chief service of being a controlled inner, that is, providing two implementations of IUnknown. One implementation forwards calls to the controlling outer, subsumed by its lifetime and identity. The other imple­ mentation is for private use of the controlling outer for actually maintaining the life­ time of and querying interfaces from the inner. To obtain the two implementations of IUnknown, CComAggObject derives from CComObj e ct Root Ex twice, once di­ rectly and once indirectly via a contained instance of your class derived from CCom- Contai nedObject, as shown here. template { p u b llc : typedef contained .BaseClass; CComAggObject(void* pv) : ii„contained(pv) { J4odule.Lock(); > -CComAggObjectC) { w 一 dwRef • 11; FinalReleaseC); | 一 Module .Unlock O ; ST0METH00(QiieryInterface)(REF1I0 iid , void ppvObject) { HRESULT hRes - S^OK; 126 ATL INTERNALS if (IniinelsEqualUnknownCiid)) { i f (ppvO bject NULL) re tu rn E_POINTER; •ppvObject * (void*)(IUnknown*)this; A ddR efO ; } e ls e hRes = •^contained. Jnterna1QueryInterface(i*id, pp\^)bjcct): return hRes; > STOMETHOO_(ULONC, AddRef)() { return Internal AddRefO; } STOMETHOO.CULONC, Release)() { ULONG 1 * Internal ReleaseO; if (1 == 0) delete this; re tu rn 1; } template HRESULT STDMETHODCALLTYPE Q u e ryIn te rfa ce (Q ** pp) { return QuerylnterfaceC 一 uuidof(Q). (void#*)pp); } s ta tic HRESULT WINAPI CreateInstance(LPUNKNOWN pUnkOuter, CComAggObject** pp); CConContainedObject n.containcd; You can see that instead of deriving from your class (passed as the template argument), CComAggOb je c t derives directly from CComObject Root Ex. Its imple­ mentation of Q uerylnterface relies on the interface map youVe built in your class, but its implementations of AddRef and Rel ease rely on the second instance of CComOb jectR oot Base it gets by deriving from CComOb j ectRootEx. This second instance of CComOb j ect RootBase uses the m_dwRef member of the union. The first instance of CComOb j ectRootBase, the one that manages the nupOuterllnknown member of the union, is the one that CComAggOb je c t gets by creating an instance of your class derived from CComContai nedObj ect as the nucontained data member. CComContai nedOb je c t implements Query In te r- OBJECTS IN ATL 127 face. AddRef, and Rel ease by delegating to the m_pOuterUnknown passed to the constructor as shown: template c la s s CComContai nedObj ect : p u b lic Base { p u b lic : typ e d e f Base _BaseClass; CComContainedObject(void* pv) { m_pOuterUnkno%vn = (IUnknown,ft)pv; > STDMETHOO(Querylnterface)(REFIIO iid , void ** ppvObject) { HRESULT hr * OuterQueryInterface(iid, ppvObject); if (FAILEDChr) SA _CetRawUnknown() U m_pOuterUnknown) h r * 二 似 广 的 川 收 … 似 广 子 狀 台 ⑴ 上 ppvObject); re tu rn h r; } STDHETHOO_(ULONC. AddRef)() { return OuterAddRefO; } STOHETHOO_(ULONG• R elease)() { return OuterReleaseO; } template HRESULT STDMETHOOCALLTYPE Q u eryInterface(Q ^* pp) { return QuerylnterfaceC 一 uuidof(Q), (vo*id**)pp); } IUnknown* G e tC o n tro l1i ngUnknownC) { return m_pOuterUnknown; } }; ' 靡 .. Being the Controlled Inner Using CComAggObject and its two implementations of IUnknown, our CPengui n class object implementation can support either standalone or aggregation activa­ tion without touching the CPengui n source: STDMETHODIMP CPenguinCO::CreateInstance(I(Jnknown* pUnkOuter, REFIID riid , void** ppv) { ☆ppv * 0; ifC pUnkOuter ) { CCoaAggObj ec t * pobj = 128 ATL INTERNALS new CConiAggObjectCpUnkOuter); 9 9 9 } e lse { CCo«Object* pobj = new CComObject and CComObject, we’ve created two classes and therefore two sets of vtbls. When you have a small number of instances or nearly all your instances are ag­ gregated, you may want a single class that can handle both aggregated and stand­ alone activation, thereby eliminating one set of vtbls. You do this by using CComPolyObject in place of both CComObj e c t and CComAggObject, like so: STDMETHOOIMP CPenguinCO::CreateInstance(IUnknown* pUnkOuter, REFIID riid , void** ppv) { *ppv = 0; CCo«iPol yObj ect * pobj = new CCo«iPol yObject (pUnkOuter); CComPolyObject is nearly identical to CComAggObject,except that in its constructor, if the pUnkOuter is zero, it w ill use its second implementation of IUnknown as the outer for the first to forward to, as shown: class CComPolyObject : public IUnknown, public CComObjectRootEx { p u b lic : ■镰參 CComPolyObject(void* pv) : _-Conta1ned(pv ? pv : t h i s ) {•••} OBJECTS IN ATL The use of CComPolyObject saves a set of vtbls, so the module size is smaller, but the price you pay for biandalone objects is getting an extra implementation of IUnknown as well as an extra pointer to that implementation. Alternative Activation Techniques Besides standalone operation, CComObject makes certain assumptions about where the object’s memory has been allocated from (the heap) and whether the ex­ istence of the object should keep the server loaded (it does). For other needs, ATL provides four more classes meant to be the most derived class in your implementa­ tion hierarchy: CComObj ectCached, CComObj ectN oLock, CComOb j e c tC l o b a l, and CComObjectStack. CComObjectCached CComObj ectCached objects implement reference counting with the assumption that you’re going to create an instance and then hold it for the life of the server, handing out references to it as requested. To avoid keeping the server running for­ ever after the cached instance is created, the boundary for keeping the server run­ ning is a reference count of 1,although the lifetime of the object is still managed on a boundary of 0,like so: template class CCoaiOb j ec t Cached : p u b lic Base { p u b lic : • • • STDMETHOO^(ULONC, AddRef)() { m_csCached. LockO ; ULONC 1 * InternalAddRefC); if (__dvvRef 2) Module.Lock(); m^csCached. UnlockO ; re tu rn ^ ; > i . : STOMETHOO^CULONC, R elease)() { m^csCached.LockC); InternalReleaseC); ULONC 1 • m^dwRef; m^csCached. UnlockO ; ■if (1 == 0) d e le te th is ; else if (1 ** 1) .Module.UnlockC); re tu rn 1; 130 ATL INTERNALS CComCIobalsThreadModel::AutoCrit1calSection icsCached; Cached objects are useful for in-process class objects: s ta tic CGwObjectCached* 9_pPengu1nC0 = 0; BOOL WINAPI DllMainCHINSTANCE, DWORD dwReason, v o id *) { s w itc h ( dwReason ) { case DLL_PROCESS_ATTACH: g_pPcnguinCO « new CCoaObj ectCached; // 1st r«f. doesn't keep server alive 1 f( g__pPenguinCO ) g_pPenguinC0->AddRefO ; break; case DLL_PROCESS__DETACH: IfC g_pP«nguinCO ) g_pPenguinCO->Rc1«ase(); break; } re tu rn TRUE; } STDAPI D11CetClassObj ect(REFCLSID clsid, REFIID riid , void** ppv) { / / Subsequent refcrcnccs do keep server alive i f ( c ls id CLSID_Penguin && g__pPenguinCO ) return g_pPenguinCO->QueryInterfaceCnid> ppv); re tu rn CLASS—E__CLASSNOTAVAILABLE; } CComObjectNoLock Sometimes you don’t want outstanding references on your object to keep the server alive at all. For example, class objects in an out-of-process server are cached in a table maintained in o l e32 • d l 1 some number of times (not necessarily just one time). For this reason, COM itself manages how the lifetime of a class object affects the lifetime of its out-of-process server using the LockServer method of the IC lassFactory interface. For this usage, ATL provides CComObjectNoLock, whose implementation does not affect the lifetime of the server. OBJECTS IN ATL template class CCosObjcctNoLock : public p u b lic : • • • STOHETHOD.CULONC, AddReOC) { return Int«ma1AddRcf (): } STDMTrHOO.CULONC, Release)( ) { ULONC 1 « InternalR«1easeO ; 1 f O ■« 0) d e lc t« th is ; re tu rn 1; Base .'K 摩 纖 • •.. /• •... . ' •• •• . • > . • -r- ,• No-lock objects are useful for out-of-process class objects: in t WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, in t ) { CoInitialize(O); CCo«ObjectNoLock4 pPengulnCO = new CCoM0bjectNoLo€k; i f ( IpPenguinCO ) re tu rn E_0UT0FMEM0RY; pPenguinC0->AddRef(); DWORD dwReg; HRESULT h r; / / Reference(s) cached by o le32.dll / / won't keep server from shutting down hr = CoRegisterClassObject(CLSID_Penguin, pPenguinCO, &dwReg); i f ( SUCCEEDED(hr) ) { MSG msg; w h i!e ( GetMessageC&msg, 0, 0, 0) ) DispatchMessage(Amsg); CoRevokeClassObj ect(dwReg); pPenguinC0->Re1ease(); > CoUninitializeC); re tu rn h r; 132 ATL INTERNALS CComObjectGlobal Just as it’s handy to have an object whose existence or outstanding references don’t keep the server alive, sometimes it’s handy to have an object whose lifetime matches that oi the server. For example, a global or static object is constructed once when the server is loaded and is not destroyed until after Wi nMai n or D11 - Mai n has completed. Clearly, the mere existence of a global object cannot keep the server running or the server would never be able to shut down. On the other hand, we’d like to be able to keep the server running if there are outstanding references to a global object. For this, we have CComObj ectG l obal • template class COMObjcctClobal : public Base { p u b lic : • * • STWETHOO^CULONC, AddRef)() { return JloduIc.LockO; } STDMETHOO„(ULONC. Rc1«as«)0《return Jtodula.UnlockO; > Global objects could be used instead of cached objects for implementing in- process class objects, but they’re useful for any global or static object // No references yet, so server not forced to stay alive s ta tic CCoaObj ectGl obal g_j>engtjinCO; STDAPI D11CetClassObj ect(REFCLSID clsid , REFIID riid , void** ppv) { // All references keep the server alive i f ( c ls id CLSID 一 Penguin ) return g^penguinCO.QueryInterface(riid, ppv); re tu rn CLASS«E_CLASSNOTAVAILABLE; } Notice that the global object usage is simpler than the cached object because we’re relying on the C runtime library to properly construct and destroy our object Au­ tomatic construction and destruction of global and static objects requires linking with the CRT, however一 something that is often avoided when building ATL-based COM servers (as discussed in Chapter 4). OBJECTS IN ATL CComObjectStack Instead of using a global or static object, you may find yourself with the urge to al­ locate a COM object on the stack. ATL supports this technique with CComObj e c t­ Stack. template class CComObj ectStack : p u b lic Base { p u b lic : • • • STDMETT400.CULONC, AddRef) O { ATLASSERTCFALSE); return 0; } .CULONG, R«leas«)0 ATLASSERT(FALSE) R«leas«)< ; return STDMETHOO(Qu«ryIntttrfacc)(REFIID 11d, void** ppvObject) { ATLASSEITT(FALSE>; rttum EJIOIMTERFACX; > Based on the implementation, it should be pretty clear you’re no longer doing COM. CComObj ectS tack w ill shut the compiler up, but you still cannot use any of the methods of IUnknown, which means you cannot pass out an interface reference from an object on the stack. This is good, because like a reference to anything on the stack, as soon as the stack goes away, the reference points at garbage. The nice thing about ATL’s implementation of CComObjectStack is that it w ill warn you at runtime (in a debug build) that you’re doing something bad. For example: void DoABadThingCIBird** ppbird) { CComObjectStack penguin; penguin.FIy(); // Using IBird method is OK penguin.StraightenTieO ; / / Using ISnappyDresser method also OK // This w ill trigger an assert at runtime penguin.QueryInterface(IID_IB1rd, (void**)ppbird); } One from Column A, Two from Column B... Table 3.2 shows the various identity and lifetime options ATL provides. ATL INTERNALS Table 3.2. ATL’s Identity and Lifetime Options Class Standalone or aggregated Heap or stack Existence keeps server alive Extant refs keep server alive CComObject Standalone He^p Yes Yes CComAggObject Aggregated Heap Yes Yes CComPolyObject Standalone or Heap Yes Yes aggregated CComObj ectC ached Standalone Heap No Second reference CComObjectNoLock Standalone Heap No No CComObjectGlobal Standalone Data No Yes segment CComObjectStack Standalone Stack No No ATL Creators Multiphase Construction As I’ve mentioned (and as youll hear more about in Chs^ter 4), ATL servers don’t often link with the CRT. However, living without the CRT can be a pain. Among other things, If we don’t have the CRT, we also don’t get C+ + exceptions. That doesn’t leave us much to do in the following scenario: / / CPenguin constructor CPenguin::CPenguin() { HRESULT hr » CoCreatelnstance(CLSID_EarthAtm osphere, 0, CLSCTX-ALL, IID .IAir, (void**)&n_pAir); if( FAILED(hr) ) { return hr; // Can't return an error from a ctor throw hr; // Can*t throw an error without the CRT II This won't help much OutputDebugString(_ T("Help! Can't bre_•.\n ")); OBJECTS IN ATL 135 The OutputDebugStri ng isn’t going to notify the client that the object it just cre­ ated doesn’t have the resources it needs to survive; that is, there’s no way to return the failure result back to the client. This hardly seems fair since the IC lass- Factory method C reatelnstance that’s creating our objects certainly can return an HRESULT. The problem is having a way to hand a failure from the instance to the class object so that it can be returned to the client. By convention, ATL classes pro­ vide a public member function called Fi nalC onstruct for objects to participate in multiphase construction: HRESULT FlnalConstructO; An empty implementation of the Final C onstruct member function is pro­ vided in CComObjectRootBase, so all ATL objects have one. Because Final - C onstruct returns an HRESULT, now we have a nice clean way to obtain the result of any nontrivial construction. HRESULT CPenguin::FinalConstruet() { return CoCreateInstance(CLSID_EarthAtmosphere, 0, CLSCTX^ALL, IID_IAi r, (void**)&n_pAir); } STDMETHODIMP CPenguinCO::Createlnstance(IUnknown* pUnkOuter, REFIID riid , void** ppv) { *ppv =: 0; if( !pUnkOuter ) { CComObject* pobj - new CComObject; i f ( 丨pobj ) re tu rn E_0UT0FMEM0RV; HRESULT hr = pobj->FinalConstructO; i f ( SUCCEEDED(hr)) . . . re tu rn h r; } We do have something else to consider, though. Notice that when Create­ lnstance calls Final Construct, it has not yet increased the reference count of the object. This causes 汪 problem if, during the Final Construct implemen­ tation, the object handed a reference to itself to another object If you think this 136 ATL INTERNALS uncommon, remember the pUnkOuter parameter to the IC1 assFactory method C reatelnstance. However, even without aggregation, it’s possible to run into this problem. Imagine the following somewhat contrived, but perfectly legal, cade: II CPenguin implementation HRESULT CPenguin::FinalConstruct(〕 { HRESULT h r; hr = CoCreateInstance(CLSID_EarthAtmosphere, 0, CLSCTX_ALL, IID_IAi r , (void**)&m_pAir); i f ( SUCCEEDEDChr) ) { II Pass reference to object with reference count of 0 hr =r RLpAir->CheckSuitabi1 i ty(GetUnknownO); } re tu rn h r; II CEarthAtmosphere implementation in separate server STDMETHOOIMP CEarthAtmosphere: :CheckSuitabi1i ty(IUnknown* punk) { IB reathe02* pbo2 = 0; HRESULT hr = E_FAIL; // CPenguin1s lifetim e increased to 1 via QI hr = punk->QueryInterface(II0.IBreathe02t (void**>4^ 602); i f ( SUCCEEDEDChr) ) { pbo2->Re1 ease(); / / During this call, lifetim e decreases to 0 ar.d / / destruction sequence begins... re tu rn (SUCCEEOED(hr) ? S—OK •• E—FA IL); } To avoid the problem of premature destruction, we need to artificially increase the object’s reference count before Fi nal C onstruct is called and then decrease its reference count afterward, like so: STDMETHODIMP CPenguinCO::Createlnstance(IUnknown* pUnkOuter, REFIIO riid , void** ppv) { *ppv = 0; ifC !pUnkOuter ) { OBJECTS IN ATL 137 CComObject* pobj = new CComObject; i f ( FAILED(hr) ) re tu rn E_OUTOFMEMORY; // Protect object from premature destruction pobj ->Internal AddRefO ; hr = pobj->FinalConstructO; pobj->InternalReleaseC); if( SUCCEEDEO(hr)) . . . re tu rn h r; } Just Enough Reference Count Safety Arguably, not all objects need their reference count artificially managed in the way I’ve just described. In fact, for multithreaded objects that don’t require this kind of protection, extra calls to Interlockedlncrem ent and In te r! ockedDecrement represent unnecessary overhead. Toward that end, CComOb j ectRootBase pro­ vides a pair of functions just for bracketing the call to Fi nal C onstruct in ajust- reference-count-safe-enough way: STDMETHODIMP CPenguinCO::CreatelnstanceCIUnknown* pUnkOuter, REFIID riid , void** ppv) { ☆ppv * 0; i f ( !pUnkOuter ) { CComObject* pobj = new CComObject; i f ( FAILED( h r) ) re tu rn E_OUTOFMEMORY; // Protect object fro« premature destruction (鶴aybe) pobj->Interna1FinalConstructAddRefC); hr = pobj->FinalConstruct(); pobj->InternalFinalConstruetRelease0 ; i f ( SUCCEEDEOChr)) . . . re tu rn h r; ATL INTERNALS By default, Internal Final Const ructAddRef and Internal Final Con stru ctR e l ease incur no release-build runtime overhead. class CCoeObjectSlootBas# { p u b lic : • • • Znt«nMil HiwlCbnstructAMtefO O void Znt«rfUi1 Fliw lC ofistm ctllttlm m O { ATLASS£RT(iu*MUf mm O '); } To change the implementation of Interna l Fi nalConstructAddRef and In te rn a l Fi nalConstructRel ease to provide reference count safety, ATL pro­ vides the following macro: fd c fin e OCOARE^PIIOrECT^FZIIAlwOQNSTKUCTO \ void InternalF1nalConstructAddRefO { InternalAddRefO; } \ void InternalF1nalConstructRelease() { InternalReleaseO; > The DECLARE_PROTECT_FINAI〜CONSTRUCT macro is used on a per-class basis to turn on reference count safety as required. Our CPengui n would use it thusly: class CPenguin : … { p u b lic : HRESULT F ina lC onstru ct(); D€CLAREJ»ROTECT„FINAL.CONSTRUCTC) In my opinion, DECLARE_J>ROTECTLFINAL_COMSTRUCT is one ATL optimiza­ tion too many. Using it requires not only a great deal of COM and ATL internals knowledge, but also a great deal of knowledge of the implementation of the objects we create in our FindlConstruc 艺 methods. Because we often don’t have that knowledge, the only safe thing to do is to always use DECLARE_PROTECT一 FINAL_CONSTRUCT if we’re handing out references to our instances in our Fi nal - C onstruct calls. And since that rule is too complicated, I imagine most folks w ill forget i t So, 111 give you a simpler one: Wherever you have a Final Const ruct member function’ you should also have a DECLARE_PROTECT_FINAICONSTRUCT macro instantiation. OBJECTS IN ATL 139 Luckily, the wizard w ill generate DECLARE—PROTECT一 FINAI_ 一 CONSTRUCT when it generates a new class, so your F inal C onstruct code w ill be safe by default. If you decide you don’t want it, you may remove it.7 Another Reason for Multiphase Construction Imagine a plain-vanilla C++ class that wishes to call a virtual member function dur­ ing its construction and another C+ + class that overrides that function: c la s s Base { p u b lic : Base() { InitO ; } virtual void InitO {} cla ss D erived : p u b lic Base { p u b lic : virtual void InitO {} Because it’s fairly uncommon to call virtual member functions as part of the con­ struction sequence, it’s not widely known that the In i t function during the cor- struction for Base w ill not be Deri ved:: In i t, but Base:: In i t. While this may seem counterintuitive, the reason it works this way is a good one: It doesn't make sense to call a virtual member function in a derived class until the derived class has been properly constructed. However, the derived class won’t be properly con­ structed until after the base class has been constructed. To make sure that only functions of properly constructed classes are called during construction, the C+ + compiler w ill lay out two vtbls, one for Base and one for Deri ved. The C++ run­ time library w ill then adjust the vptr to point to the appropriate vtbl during the con­ struction sequence. While this is all part of the official C+ + standard, it's not exactly intuitive, es­ pecially because it is so rarely used (or maybe it’s so rarely used because it’s un­ intuitive). Because it’s rarely used, Microsoft introduced _ decl spec (n o v tb l) in Visual C+ + 5.0 to turn off the adjustment of vptrs during construction. When the base class is an abstract base class, this w ill often result in vtbls that are generated by the compiler but not used, so the linker can remove them from the final image. 7 As my friend Tim Ewald always said, 'Subtractive coding is easier than additive coding. 140 ATL INTERNALS This optimization is used in ATL whenever a class is declared using the ATI〜 NO^VTBL macro: fifd e f JMUDISABUJW.VTBL #define ATL-NO^VTBL •else # defi ne ATL.WO^VTBL — dec1 spec(novtbl) #endif Unless the _ATL_DI SAB L E_NO_VTB L is defined, a class defined using _ATL_N0_ VTBL will have its constructor behavior ac\justed with _ dec! spec (n o vtb l): cla ss ATL.NO.VTBL CPenguin . . . { } ; While this is a good and true optimization, classes that use it must not ca ll virtual member functions in their constructors.8 Rather than calling virtual member func­ tions during construction, call them in F inal Const ru ct, which ATL calls after the most derived class’s constructor executes and after the compiler ac(justs the vptrs to the correct values. There is one last thing to mention about 一 dec! spec (n o v tb l) • Just as it turns off adjustment of vptrs during construction, so does it tum off adjustment of vptxs during destruction. Therefore, avoid calling virtual functions in the destruction as well; instead, call them in the object’s Fi nal Rel ease member function. Final Release ATL w ill call the object’s Fi nal Rel ease function after the object’s final interface reference is released and before your ATL-based object’s destructor is called: void Final ReleaseO; The Fi nal Rel ease member function is useful for calling virtual member functions and when releasing interfaces to another object that also has pointers back to you* Because that other object may wish to query for an interface during its shutdown sequence, it’s just as important to protect the object against double destruction as it was to protect against premature destruction in Fi nal Const ru ct. Even though the Fi nal Rel ease member flmction w ill be called after the object's reference 8 Strictly speaking, the compiler will statically bind to virtual calls made in the constructor or the de­ structor, but if a statically bound function calls a dynamically bound function, you're still in big trouble. OBJECTS IN ATL count has been decreased to 0 (which is why the object is being destroyed), the caller of Fi nal Rel ease w ill artificially set the reference count to 1 to avoid double deletion. The caller of Final Rel ease will be the destructor of the most derived class. For example: CCofiObject::-CComObject() { ■.divRsf » 1L; F1na1R«1eas«(); _Hodule•Uniock(); > ATL Creators Because the extra steps to manage the multiphase construction process are easy to forget, ATL encapsulates this algorithm into several C+ + classes called creators, each of which performs the appropriate multiphase construction. Each creator class is really just a way to wrap a scope around a single static member function called Createlnstance: sta tic HRESULT WINAPI Createlnstance(void* pv, REFIID r i i d , LPVOID* ppv); The name of the creator class is used in a type definition associated with the class, as 111 discuss later. CComCreator CComCreator is a creator that creates either standalone or aggregated instances and is parameterized by the C++ class being created, for example, CCom- Object. CComCreator is declared as follows: template class COMCrcator { p u b lic : sta tic HRESULT WINAPI Cr#at«Instafic«(void* pv, REFIID r i i d , LPVOID* ppv) { ATLASSERT(*ppv NULL); HRESULT hRes • E_OUTOfMEHORY; T I* p - NULL; ATLTRYCp - new T l(p v )) 1 f (p ! - NULL) { p->SetVoid(pv); ATL INTERNALS p->Intem al Fi nalConst ructAddRef O ; HRes - p->FinalConstructO ; pinternalFinalConstructReleaseO; • S.OK) hRes • p->Qu«ryXnterface(n*id, ppv); » S_OK) d e le te p; i f (hRes i f (HRes I } return hRes; Using CComCreator simplifies our class object implementation quite a bit: STDMETHODIMP CPenguinCO::Createlnstance(IUnknown* pUnkOuter, REFIID riid , void** ppv) { typedef CCo«Creator< CComPoI yObject > PenguinPolyCreator; return PenguinPolyCreator::CreateInstanceCpUnkOutert riid , ppv); } Notice the use of the type definition to define a new creator type. If we were to create penguins other places in our server, we’d have to rebuild the type definition. For example: STDMETHODIMP CAviary: :CreatePenguin(IBi rd** ppbird) { typedef CComCreator< CG>aObject > Pengui nC reator; return PenguinCreator::Createlnstancc(0, II0_lBi rd, (void**)ppbird); > Defining a creator like this outside of the class being created has two problems. First, it duplicates the type definition code. Second, and more important, we’ve taken away the right of the CPengui n class to decide for itself whether it wants to support aggregation or not, since the type definition is making this decision now. By convention in ATL, to reduce code and to let the class designer make the decision about standalone versus aggregate activation, we place the type definition inside the class declaration and give it the well-known name 一 CreatorCI ass: class CPenguin : … { p u b lic : OBJECTS IN ATL typedef CCo«Creator< CCowiPolyObject > _CreatorC1ass Using the creator type definition, creating an instance and obtaining an initial in­ terface actually takes fewer lines of code than when using operator new and Q u e ry ­ ln t e r f a c e : STDMETHODIMP CAviary::CreatePenguin(IBird** ppbi rd) { return CPenguin::„CreatorClass::Createlnstance(0, I I D J B ir d , (void**)ppbird); } In Chapter 4, I’ll discuss one other base class that your class w ill often derive from: CComCoCl ass. For example: class CPenguin : •••, public CComCoClass, . . . CComCoClass provides two static member functions, each called C reateln- stance, that make use of the class’s creators like so: template c la s s OCoaCoClass { p u b lic : # • • teiiplate s ta tic HRESULT Cr«ftttZvist«noi(IUnknown# punkOuter, Q** pp) return T::«CreatorClass::Cr€ateInstance(punkOuter, 一 uu1dof(Q)f (void**) pp); } template s ta tic HRESULT CrMt«lnstaiic«CQ** pp) i return T: :.CreatorClass: :CreateInstance(NULLv —uui dof CQ). (void**) pp); 144 ATL INTERNALS This simplifies our creation code still further: STDMETHODIMP CAvnary: :CreatePenguin(IBird** ppbird) { return CPenguin::Createlnstance(ppbird〕; } CComCreator2 It may be that we’d like to support both standalone and aggregate activation us­ ing CComObject and CComAggObject instead of CComPolyObject because of the overhead associated with CComPolyObject in the standalone case. To avoid putting the logic of choosing the appropriate creator back into the class object’s C reatelnstance method based on whether we have a NULL pUnkOuter or not, ATL provides CComCreator2: template class CComCreator2 { p u b lic : s t a t ic HRESULT WINAPI C re a te ln s ta n c e (v o id * pv, REFIIO r i i d , LPVOID* ppv) { ATLASSERT(Appv == NULL); return (pv == NULL) ? T I::Createlnstance(NULL, riid , ppv) : T2::CreateInstanceCpv, riid , ppv); Notice that CComCreator2 is parameterized by the types of two other creators. All CComCreator2 does is check for a NULL pUnkOuter and forward the call to one of tw o other creators. So, if we'd like to use CComObject and CComAggObj e c t instead of CComPol yObj e c t, we can do so like this: class CPenguin : … { p u b lic : • • • typedef CCo«Creator2< CCo«Creator< CCotObj ect >, CCo««Creator< CConAggOb j ect > > .CreatorClass; Of course, the beauty of this scheme is that, because all the creators have the same function, Createlnstance, and are exposed via a type definition of the same name, _CreatorClass, none of the server code that creates penguins needs to OBJECTS IN ATL 145 change if the designer of the class changes his or her mind about how penguins should be created. CComFailCreator One of the changes we may want to make to our creation scheme is to support ei­ ther standalone or aggregate activation only, not both. To make this happen, we need a special creator to return an error code to use in place of one of the creators passed as template arguments to CComCreator2. That’s what CComFailCreator is fo r tempi ate class CCo«Fai1C reator { p u b lic : sta tic HRESULT WINAPI CreateInstance(void*, REFIID, LPVOID*) { re tu rn h r; } }; If we’d like standalone activation only, we can use CComFai 1 C reator like this: class CPenguin : … { p u b lic : « » « typedef CComCreator2< CComCreator< CComObject >, CConFai 1 Creator > —CreatorCI ass; If we'd like aggregate activation only, we can use CComFai 1 C reator like this: class CPenguin : … { p u b lic : • • • typedef CComCreator2< CCoaFai1Creator, CComCreator< CComAggObject > > 一CreatorCIass; }; Convenience Macros As a convenience,ATL provides the following macros in place of manually specify­ ing the ^C reatorC I ass type definition for each class. 146 ATL INTERNALS 參de fine 0ECLAREJ>0LYJWCRECATA8LE(x) p u b lic : 、 typedef CConCreator< CComPolyObjcct< x > > ^CreatorClass; *def1ne DECLAREJWCRECATABLE(x) p u b lic : 、 typedef CComCreator2< CComCreator< CComObjcct< x > >, CCo«iCreator< CComAggObject< x > > > .CrcatorClass; ♦define OECLAREJ«TJMX»ECATABLECx) p u b lic : 、 typedef CComCreator2< CCoiiCreator< CComObject< x > >, CCMFai lCreator -CreatorClass; *d e fin e DECURE^ONLYjiOCRECATABLECx) p u b lic : 、 typedef CCo«Creator2< CCoaFailCreator, CCo«Creator< CComAggObject< x > > > ^CreatorClass; Using these macros, I can declare that CPengui n may be activated both standalone and aggregated as follow s: class CPenguin : … { p u b lic : • • • DECLARE^ACCREGATABLECCPenguin) Table 3.3 summarizes the classes that the creators use to derive from your class. Table 3.3. Creator Type Definition Macros Macro Standalone Aggregation DECLARE^ACGRECATABLE CComObject CComAggObject DECLARE_NOT^AGGREGATABLE CComObject N/A DECLARE_ONLY_AGGREGATABLE N/A CComAggObject DECLARE_POLYJ\GCRECATABLE CComPolyObject CComPoTyObject OBJECTS IN ATL Private Initialization Creators are handy because they follow the multiphase construction sequence used by ATL-based objects. However, creators only return an interface pointer, not a pointer to the implementing class (that is, IB i rd* instead of CPengui n*). This can be a problem if the class exposes public member functions or member data that is not available via a COM interface. Your first instinct as a former C programmer may be to simply cast the resultant interface pointer to the type you'd like: STDMETHODIMP CAviary::CreatePenguin(BSTR bstrName, long nWingspan, IB ird** ppbird) { HRESULT h r; hr = CPenguin::_CreatorClass: :CreateInstance(0, IID .IB ir d , (void**)ppbird); i f ( SUCCEEDEDChr) ) { CPenguin* pPenguin = (CPenguin*)(*ppbird); / / Resist this instinct! pPenguin->Ini t(bstrName, nWingspan); } re tu rn h r; Unfortunately, because Q uerylnterface allows interfaces of a single COM iden­ tity to be implemented on multiple C+ + objects or even multiple COM objects, in many cases a cast won’t work. Instead, you should use the C reatelnstance static member functions of CComObject, CComAggObject, and CComPolyObject: s ta tic HRESULT WINAPI GCooObjcct:: CrM t«lnstan c« (CComObj ec t * * pp); s ta tic HRESULT WINAPI CCoeAggObject::CrsateInstance(IUnknown* puo» CComAggObject* • p p ); s ta tic HRESULT WINAPI CGiaPolyObjcct: :Cr€at^Insta«K«CIUnknown* puo(IUnknown* CComPolyObject** pp) These static member functions do not make creators out of CComObj e ct, CCom­ AggObject, or CComPolyObject,but they each perform the additional work 148 ATL INTERNALS required to call the object’s Fi nal Const ru c t member function. The reason to use them, however, is that each of them returns a pointer to the most derived class. For example: STDMETHODIMP CAviary::CreatePenguin(BSTR bstrName, long nWingspan, IB ird** ppbi rd) { HRESULT h r; CComObject* pPenguin = 0; hr = CCo«Object::CreateInstance(ftpPenguin); i f ( SUCCEEDED(hr) ) { pPengui n->AddRef(); pPenguin->Init(bstrName, nWingspan); hr «= pPenguin->QueryInterface(IID_IBi rd, (void**)ppbi「d ) ; pPengui n->ReleaseC); > re tu rn h r; } The class you use for creation in this manner depends on the kind of activa­ tion you want. For standalone activation, use CComObj e c t:: Createlnstance. For aggregated activation, use CComAggObject::Createlnstance. For either standalone or aggregated activation that saves a set of vtbls at the expense of per- instance overhead, use CComPolyObject::Createlnstance. Multiphase Construction on the Stack When creating an instance of an ATL-based COM object, you should always use a creator (or the static Createlnstance member function of CComObj ect and friends) instead of the C ++ operator new. However, if you have a global or 压 static object or an object that’s allocated on the stack, you can’t use a creator, because you're not calling new. As I discussed earlier, ATL provides two classes for creating instances that are not from the heap: CComObj ectG l obal and CComObj ectS tack. > call F in a l Con: lasses w in perform the proper destru< However, instead of requiring you to call Fi nal C onstruct (and Fi nal Rel ease) manually, both these classes w ill perform the proper initialization and shutdown in their constructors and destructors, as shown here in CComObj ectG l obal. template class COMObjcctClobal : p u b lic Base { p u b lic : typedef Base JaseClass; OBJECTS IN ATL 149 COMObj ec t Global (v o id * ■ NULL) { « J yK€sF1 fial Construct * FI nal Const ruct Q ; > -CCoaObj ectCl obal () { F1na1R< HRESULT telMiseO; > O ilU s F ln a K Because there is no return code from a constructor, if you’re interested in the result from Fi nalC onstruct, you must check the cached result in the public member variable m_hResFi nalC onstruct. Debugging ATL provides a number of helpful debugging facilities, including both a normal and a categorized wrapper for producing debug output, a macro for making assertions, and debug output for tracing calls to Q uerylnterface, AddRef, and Rel ease on an interface-by-interface basis. Of course, during a release build, all these debug­ ging facilities fall away to produce the smallest, fastest binary image possible. Making Assertions Potentially the best debugging technique is the use of assertions. Assertions allow you to make assumptions in your code and to be notified immediately when those assumptions are invalidated. While ATL doesn’t exactly support assertions, it does provide the ATLASSERT macro. However, it’s really just another name for the Micro­ soft CRT macro ^ASSERTE. #ifnd€f ATLASSERT MLASSERT(fd e fln e ATLASSERT(expr) JkSSERTE(expr) #cnd1f Flexible Debug Output OutputDebugSt r i ng is handy as the Win32 equivalent of p ri n tf, but it only takes a single string argument. What we want is a p ri n tf that outputs to debug output instead of standard output, and that’s what A t 1 Trace is fo r void A t!T rmcmCtPCTSTR IpszF om at, •••) 150 ATL INTERNALS string and the variable level. The trace category ned categories • 0x0( • 0x< • 0x00000004, • 0x00000008, (00000001, (00000002, In addition to the format trace category and a trace €num atlTrsc^Fla^s { // Application-defi atlTraceUser atlTraceiiser2 atlTraceUser3 |4atlTraceUser4 / / ATL-defined cate atlTraceCeneral atlTraceCOW atlTraceQ I atlTraccRegistrar atlTraceRefcount a t1TraceWindowing atlTraceControls atlTraceHosting atlTrace06C1i ent a t1TraceOBProvider atlTraceSnapIn atlTraceNotlnpl If you’d like to be even more selective about ATL provides a second trace function, A tlT race 2 TRACE2. output, o. ATL- takes a ilowing: akes it to debug ith its own macr nts, AtlTrace2 3r more of the fol what m !, also w argume: i is one • However, instead of using A tlT race directly, ATL provides ATLTRACE, a macro that either expands to a call to A tl Trace or expands to nothing, depending on whether the _DEBUG symbol is defined or not Typical usage is as follows: HRESULT CPenguin::Final Construct() { ATLTRACE(— T("%d+%d= %d\nM), 2, 2, 2+2): > void AtlTrace2(0W0R0 category, UINT level, LPCTSTR IpszFonnat, ...); 20 40 80 00 00 00 00 ‘ 111 = •ie ox ox oxs oxg ox ox ox ox ox ox OBJECTS I N ATL The trace level is a measure of severity, with 0 the most severe. ATL itself uses only levels 0 and 2,and the documentation recommends that you stay between 0 and 4, but you can use any level up to 4,294,967,295 (although that may be a little too fine- grained to be useful). A tIT ra ce 2 matches the category and the level against server- wide definitions of ATL_TRACE_CATECORY and ATL_TRACE_LEVEL: void At1Trace2(DWORD category, UINT le v e l, LPCTSTR IpszFormat. . . . ) { 1f (category • ATkJRAdCATEGORY M l«v«1 ATLJTRACEJ^VEL) { … // ouput code removed for brevity > > If ATL finds ATL_TRACE_CATECORY or ATL__TRACE_LEVEL undefined, it w ill pro­ vide its own defaults: #1fnd«f ATU.TRACE.CATEC30inr 参d e fin e ATL.TRACL.CATEOORY OxFFFFFFFF / / Output a ll categories #« n d if #ifnd«f ATljnUCE^UVEL 參define ATL-TRACE^LEVEL 0 // Output most severe only # e n d if Typical usage allows fine-grained control of debug output: STDMETHOOIMP CPenguin:: F ly () { ATLTRACE2(atlTraceUser, 2, _T("IBird::Fly\n")); ATLTRACE2(atlTracellserf 42, .. Penguins can't fly .. An ")); ATLTRACE2Cat1TraceNotImp1, 0, _ T (" IB ir d ::F ly not im p 1 e _e n te d !\n ")); re tu rn E—NOTIMPL; } In fact, because ATL uses the category atlTraceN otlm pl so often, there’s even a special macro for it: ATLTRACOiOTIMPL(f^iaiMM) \ ATLTKACE2(atlTraceNotlmpl, 2, JT(”ATL: Xs fK)t 1np1emented.\nK) » \ funcnaae); \ re tu rn E.N0TIHPL 152 ATL INTERNALS This macro is used a lot in the implementations of the OLE interfaces: ST0M£TH00(S«tMoniker)(DWORD, IM oniker*) { ATLTRACEW0TIMPtCT(,lI0 1 e 0 b jtctI» p l: :S etltenik«r")〉; 薩 When you’re interested in the interaction between the client and your object’s implementation of standard interfaces, you w ill want to define ATI>•TRACE—LEVEL to 2 before compiling. ATL’s interface implementation classes make heavy use of level 2 to provide tracing of individual method calls. Tracing Calls to Querylnterface ATL’s implementation of Q uerylnterface is especially well instrumented for de­ bugging. When you define the _ATI»_DEBUCLQI symbol before compiling, your ob­ jects w ill output their class name, the interface being queried for (by name,9 if available), and whether the query succeeded or failed. This is extremely useful for reverse engineering clients, interface requirements. For example, here’s a sample of the _ATIDEBUG 一 QI output when hosting a control in Internet Explorer (IE) 4: CComClassFactory - IUnknown CComC lass Factory *- IClassFactory CPenguin - IUnknown CPenguin - IO leControl CPenguin - IClientSecurity - failed CPenguin - IQuickActivate - failed CPenguin - 101eObject CPenguin - 101eObject CPenguin - IPersistPropertyBag2 - failed CPenguin - IPersistPropertyBag - failed CPenguin - IPersistStreamlnit CPengui n - IID _ IV i ewObj ectEx CPenguin - IConnectionPointContainer - failed CPenguin - IA c tiv e S c rip t - fa ile d CPenguin - {6DS140D3-7436-11CE-8034-OOAA006009FA) • fa ile d% CPenguin - IOleControl CPenguin - 101eCommandTarget • fa ile d CPenguin • IDispatchEx - failed ®Interface name9 for remotable interfaces are available in the registry as the default value of the HKEY_ CLASSES^ROOT\Interfaces\{IID} key. OBJECTS IN ATL CPengui n - ID isp a tch CPenguin -101eObject CPenguin -101eObject CPenguin - IRunnableObject - fa ile d CPenguin - IOleControl CPenguin -101eObject CPenquin -101elnPlaceObject CPenguin - IID_I01elnPlaceObjectWindowless CPenguin -1 01 elnPlaceActi veObj ect CPenguin - IOleControl CPenguin - IClientSecurity - fa ile d CPenguin - IClientSecurity - failed Tracing Calls to AddRef and Release The only calls more heavily instrumented for debugging than Query In te rfa c e are AddRef and Release. ATL 3.0 introduced quite an elaborate scheme for tracking calls to AddRef and Release on individual interfaces. The reason it is elaborate is that each ATL-based C++ class has a single implementation of AddRef and Release, implemented in the most derived class, for example, CComObject. To overcome this lim itation, when _ATL_DEBUG_INTERFACES is defined, ATL wraps each new interface10 handed out via Que ry In te r f ace in another C + + object that implements a single interface. Each of these thunk objects keeps track of the real interface pointer as well as the name of the interface and the name of the class that has implemented the interface. The thunk object also keeps an interface-pointer- specific reference count, in addition to the object’s reference count, that the thunk object’s implementation of AddRef and Rel ease manages. As calls to AddRef and Rel ease are made, each thunk object knows exactly which interface is being used and dumps reference count information to the debug output. For example, here’s the same interaction between a control and IE4, but using _ATL_DEBUG_INTER- FACES instead of _ATL_DEBUC_QI: 1> CComClassFactory - IUnknown 1> CComClassFactory - IClassFactory 1> CPenguin - IUnknown 1> CPenguin - IOleControl 0< CPenguin - IO le C o n tro l 1> CPenguin - 101eObject 10 ATL makes sure to always hand out the same thunk for each object’s IUnknown* to observe the rules of COM identity, as discussed in Chapter 5. 154 ATL INTERNALS 1> CPengui n 0< CPenguin 0< CPenguin 1> CPenguin 0< CPenguin 1> CPenguin 1> CPenguin 0< CPengui n 1> CPenguin 1> CPengui n 1> CPenguin 0< CPenguin 0< CPenguin 1> CPenguin 0< CPenguin 101eObject 101eObject IOleObject IPersistStreamlnit IPersistStreamlnit IID_IViewObjectEx 101eControl IOleControl ID isp a tch IOleObject IOleObject 101eObject IOleObject 101eControl 101eControl 0< CComClassFactory - ICIassFactory 1> CPenguin • IOleObject 1> CPenguin 1> CPenguin 1> CPenguin 0< CPenguin 0< CPengui n 0< CPenguin 1> CPenguin 0< CPengui n 101elnPlaceObject IID —IOlelnPlaceObjectWindowless 101elnPlaceActiveObject 101elnPlaceActiveObject 101elnPlaceObject IO leO bject 101eControl 101eControl ATL maintains a list of outstanding thunk objects. This list is used at server shutdown to detect any leaks,that is, any interfaces that have not been released by the client. When using _ATL_DEBUG一INTERFACES, watch your debug output for the string INTERFACE LEAK, which is an indication that someone has mismanaged an interface reference. For example: INTERFACE LEAK: RefCount * 1, MaxRefCount = 1, {Allocation = 4} CPenguin - ISnappyDresser The most useful part of this notification is the Allocation number. You can use this to track when the leaked interface is acquired by setting the m^nlndexBreakAt member of the CComModul e at server start-up time. For example: e xte rn "C" BOOL WINAPI DllMainCHINSTANCE h ln sta n ce , DWORD dwReason, LPVOID) { i f ( dwReason -= DLL.PROCESS^ATTACH) { OBJECTS IN ATL 155 —Module.Init(Ob]ectMap, hlnstance, &LIBID_PISVRLib); DisableThreadLi braryCal1s(hlnstance); // Track down interface leak ..Hodul e . m_nIndexBreakAt = 4; > e lse i f (dwReason -= DLL_PROCESS_DETACH) _Module.Term(); return TRUE; // ok > When that interface thunk is allocated, the _Modul e w ill call DebugBreak, handing control over to the debugger and allowing you to examine the call stack and plug the leak. _ATL-DEBUG 一 REFCOUNT Versions of ATL prior to 3.0 used the _ATL_DEBUG_REFCOUNT symbol to track interface reference counts for ATL IXxxIm pl classes only. Since _ATL_DEBUG一 INTERFACES is so much more general, it has replaced _ATL_DEBUG一 REFCOUNT, although _ATL_DEBUG_REFCOUNT is still supported for backward compatibility. f i f d e f .ATUJ)EBUCJIEFCOUNT # ifn d e f _ATLJ)EBUCLIWTERFACES fd«f1ne JML-DEBUCLINTERFACES fe n d if Summary ATL provides a layered approach to implementing IUnknown. The top layer, repre­ sented by the CComXxxThreadModel classes, provides helper functions and type definitions for the synchronization required by objects residing in both STAs and MTAs. The second level, CComObjectRootEx, uses the threading model classes to support just-thread-safe-enough AddRef and Release implementations and object-level locking. Its base class, CComObj ectRootEx, also provides a table- driven implementation of Q uerylnterface, using an interface map provided by your class. Your class derives from CComObj ectRootBase and any number of in­ terfaces, providing the interface member function implementations. The final level is provided by CComObject or one of its friends, which provides the implementa­ tion of Q uerylnterface, AddRef, and Rel ease based on the lifetime and identity requirements of the object. 156 ATL INTERNALS To allow each class to define its one lifetime and its identity requirements, each class defines its own 一 C reatorCl ass, which defines the appropriate creator. The creator is responsible for properly creating an instance of your ATLrbased class and should be used in place of the C+ + operator new. Finally, to debug your objects, ATL provides a number of debugging facilities, including tracing and interface usage and leak tracking. CHAPTER 4 COM Servers A Review of COM Servers Once you’ve created one or more COM classes, you need to package all the classes together and install them on a system. The package is called a COM server. A COM server is a dynamically activated collection of the implementations of one or more COM classes. Modem Win32 versions o f COM allow you to create a COM server as an in-process (inproc) server (a dynamic link library), an out-of-process server (an executable), or, on the Windows NT operating system, as a system service exe­ cutable.1 Some people mean a COM server when they refer to a COM component. (Others mean a COM class when they refer to a COM component, so I’ll use COM server.) A COM server has three jobs, in addition to hosting the implementations of its classes: 1. Register and unregister all classes in the server and, potentially, the server itself. 2. Provide the COM Service Control Manager (SCM) access to all COM class ob­ jects implemented by the server (often called exposing the class objects). Class objects are frequently called class factory objects because they generally im­ plement the IC1 assFactory interface. 3. Manage the server’s lifetime. This typically means allowing the SCM to unload an inproc server from memory when it’s no longer used. A local server often terminates when there are no more references to objects managed by the server. Although technically the first and third items are optional, all COM servers should implement this functionality. How, exactly, a server does this depends on the type of server. 1 Here "Modem Win32 versions of COM” refers to Windows NT 4.0 and greater, Windows 98, and Win­ dows 95 with the DCOM95 upgrade. 158 ATL INTERNALS Inproc Servers An inproc server is a dynamic link library (DLL) containing five well-known entry points一 one used by the Win32 operating systems and the other four used by COM: BOOL WINAPI 011Main(MINSTANCE h ln s ta n c e , DWORD dwReason, STDAPI OilRegisterServer(void); STDAPI OllUnreglsterServer(vo1d); STDAPI 0nCetClass0bject(REFCLSID rclsid. REFIID riid , LPVOIO* ppv); STDAPI 011CanUnloadNow(void); Each of the Win32 operating systems calls a dynamic link library’s D11 Mai n func­ tion when it loads a DLL into a process and removes a DLL from a process. The op­ erating system also calls D11 Mai n each time the current process creates a new thread and when a thread terminates cleanly. This function is optional but present in all ATL inproc servers. The D llR egisterServer and DITUnregisterServer functions create and remove, respectively, all entries in the Windows registry that are necessary for the correct operation of the server and its classes. When you use the REGSVR32 utility to register or unregister an inproc server, the utility loads the DLL and calls the ap­ propriate one of these functions in the DLL. Technically, both of these functions are optional, but, in practice, you want to have them in yo rver. The SCM calls a server’s D11 GetCl assOb j ect tion when it requires a class object exposed by the server. The server should return the requested inter­ face, r i i d, on the specified class object, re l s i d When you call the CoFreeUnusedLi b raries API, COM asks each inproc server in your process if COM should unload the DLL by calling the D ll Can­ Unl oadNow function. An S一OK return value means that the server permits COM to unload the server. An S—FALSE return value indicates that the server is busy at the moment and COM cannot unload it. Local Servers and Service-Based Servers A local server or NT service is an executable image (EXE) server containing one well-known entry point: extern "C" int WINAPI .tWinMainC hPrevInstance, LPV0I0 IpReserved) COM SERVERS Executables cannot provide multiple specialized entry points, unlike DLLs, so a lo­ cal server must use a different technique to implement the three requirements of a COM server: registration, exposing class objects, and lifetime management. A local server registers or unregisters itself and then immediately terminates when the server parses its command line and finds the well-known (case-insensi­ tive) command-line switches RegServer or UnregServer, respectively. A local server exposes its class objects by calling the CoRegi sterC l ass- Ob j e c t API and handing an IUnknown interface pointer to each of the server’s class objects to the SCM. A local server must call this API after the SCM starts the server process.2 When the server is ready to shut down, it must first call CoRevokeCl ass- Ob j e ct to notify the SCM that each of the server’s class objects is no longer avail­ able for use. A local server manages its own lifetime. When a server detects that there are no references to any of the objects managed by the server, the server can shut down (or not) as it wishes. As you’ll see later in this chapter, detecting that there are no references is a little trickier than you might think. COM Createable and Noncreateable Classes A COM createable class is a COM object class that supports a client creating in­ stances of the class using the CoCreatelnstance API. This implies that the class must provide a class object and that the class object implements the IC1 assFac- to ry interface. A noncreateable class typically provides no class object, so calling CoCreate- In stance using the class's class identifier (CLSID) fails.3 In many designs, there are far more noncreateable classes than there are createable ones. For example, in one version of Microsoft Excel, there were roughly 130 classes, only 3 of which were createable. The Object Map and the CComModule Class ATL uses two constructs to support the functionality required by all types of servers: the object map and the CComModul e class. The object map (more properly entitled a class map) is a table of all classes implemented in the server. 2 As of this writing, a local server must register its class objects within 120 seconds. The time was less in previous software releases. It might change again in the future, so simply register your class objects as quickly as possible. 31 don't know how many times people have told me “Noncreateable classes do not have CLSIDs.” Some­ times they do. When you want to describe a noncreateable class in a type library, you must assign the coclass a CLSID. MIDL won't compile a coclass statement, even one with the noncreatable at­ tribute, unless the class has the uuid attribute. (Note also that MIDL uses a different spelling一“non- creatable"一than other areas of COM.) 160 ATL INTERNALS Various methods o f the CComModul e class use the object map to ■ Find each class and ask it to register and unregister itself. ■ Create the class object, if any, for each createable class. ■ Register and unregister the class objects with the SCM. ■ Find a table of implemented and required component categories for a class. ■ Call each class’s static initialization and termination member function. The basic idea is that you describe the classes that you are implementing in a server using the object map. Whenever you need basic server functionality, there is prob­ ably a method of the CComModul e class that implements much, if not all, of the re­ quired functionality. Many CComModul e methods iterate over the entries in the object map and ei­ ther ask each class to perform the required function or ask each class to provide the inform ation needed to allow the CComModul e method to do it. The Object Map Here’s an example of an object map called ObjectMap that defines three classes. You start defining the object m^> using the BECIN_0BJ ECT_MAP macro, populate the table using the OBJECT.ENTRY and the OB 3 ECT.ENTRY.NON_CREATEABLE macros, and end the definition using the END_0BJECT_MAP macro. In the following example, the object map describes three classes. Clients can call CoCreatelnstance to create instances of the CDemagogue and CEarPol i t i c classes (that is, they are COM createable classes) but cannot for the CSoapBox class.4 BECIN.OB3Eaj4AP(ObjectHap) OBJ ECT_ENTRY(CLSID.Demagogue, COemagogue) 0B3Ea«ENTRY(CLSI0.EarPolitic, CEarPolitic) 0B5Ea«ENTRY.M0N.CREATEABLE(CSoapBox) END一 OBJECTJ4AP() You define a COM createable class using the OBJ ECT_ENTRY macro. You define a noncreateable class using the OBJ ECT一 ENTRY一 NON 一 CREATEABLE macro. (Big 4 For those curious, I decided that we needed a new term for something or someone that listens to the Body Politic, and clearly this should be an Ear Politic. COM SERVERS surprise there, I bet!) The ATL infrastructure starts with the assumption that you’ll always have an instance class (you create instances of this class). An instance class may or may not have an associated class object class. The first assumption supports noncreateable objects. An instance class that has an associated class object can be a createable class. However, the ATL object map currently has no support for a standalone class object, that is, a class object that never creates any instances of its class. In ATL you must first define an instance class; then, within the instance class declaration, you can define the class of its class object. At runtime, you can fail requests for IC la ss­ Factory or, preferably, support IC lassFactory and fail all instance creation requests unless your custom class object class implements the IExternalC on- nection interface to track external references to the class object.5 The Object Map Macros BEGINJDBJECT一MAP Macro The BECIN_0B3ECT_MAP macro begins the array definition. The macro parameter specifies the name of the array. ^define BECIN^OBJECT^MAP(x) s t a t ic «ATL_OB]MAP 一 ENTRY x [ ] - { 一ATL一OBJMAP_ENTRY Structure The object map is an array of _ATI__OBJ MAP一 ENTRY structures that looks like this: s tru c t ^ATL«0B3MAP_ENTRY { const CLSID* pci sid; HRESULT (WINAPI *pfnUpdateRegi stry)(BOOL bRegister); _ATL.CREATORFUNC* pfnG etC lassO bject; _ATL_CREATORFUNC* p fn C re a te ln s ta n c e ; IUnknown* pCF; DWORO dwRegi s te r; ^ATL^OESCRIPTIONFUNC* p fn C e tO b je c tO e s c rip tio n ; -ATL.CATMAPFUNC * pfnGetCategoryMap; void (WINAPI ♦pfnObjectHain)(bool bStarting); 6You should implement IC lassFactory even when you want to fail instantiation requests so that the stub can call LockServer when remoting references to the class object 162 ATL INTERNALS The structure contains the following fields, many of which are pointers to functions: p c i s id pfnllpdateRegi stry pfnGetClassObject pfnCreatelnstance PCF d w R e g iste r pfnCetObj ectDesc rip tio n pfnCetCategoryMap pfnObjectMain Pointer to CLSID for this class entry. The function that registers and unregisters the class. The creator function that creates an instance of the class object The creator function that creates an instance of the class. Pointer to the inproc class object instance; NULL if not yet created. Registration cookie returned by CoRegi s te r- ClassObject. The function that returns the object descrip­ tion fo r the class. The function that returns the component cate­ gory msjp. The class initialization /termination function. END一 OBJECT—MAP Macro You use the END_OBD ECT_MAP macro to add a terminating entry to the array and end the array definition. •define EN0_0B3ECLMAP() \ {NULL, NULL. NULL. NULL. NULL. NULL, NULL, NULL}}; Each OBJEa.ENTRY and 0B3 ECT_ENTRY_NON.CREATEABLE macro adds one _-ATL_OB J MAP_ENTRY structure to the array. OBJECT一 ENTRY Macro You use the OB] ECT_ENTRY macro to specify a COM createable class. Topically, this means the specified class derives from the CComCoClass base class. Often these are top-level objects in an object model. Clients typically create such top-level ob­ jects using CoCreatelnstance. ♦define 083Ea.ENTRY(cl$id, class) \ {Aclsid, class::UpdateRegistry, \ class::.ClassFactoryCreatorC1ass::CreateInstanccf \ COM SERVERS 163 class::_CreatorClass::CreateInstance, NULL, 0, \ class::CetObjectDescription, class::CetCategoryMap, \ class::0bjectMain }• OBJECT_ENTRY_NON_CREATEABLE Macro You use the OB J ECT_ENTRY_NON_CREATEABLE macro to specify a class that does not have an associated class object. Often these are non-top-level objects in an object model. Clients typically must call a method on a higher-level object in the object hierarchy to obtain an instance of this class. Because the specified class does not have an associated class object, clients cannot create an instance by calling CoCreatelnstance. # d « fin e 0e3ECT_ENTRY一 NON—CREATEABLE(cl ass) \ {ACLSID^NULL, class: ilipdateRegistry, \ NULL. NULL, NULL, 0, NULL, class::C etC ategoryM ap, \ class::0bjectMain }, You w ill use the OBJ ECT_ENTRY_NON_CREATEABLE macro prim arily fo r non­ createable classes that need class-level initialization and uninitialization. Occasion­ ally, you might want to have a noncreateable class maintain registry entries and possibly persistent class configuration information and/or component categories. Methods Required of an Object Map Class The CComModule class registers, unregisters, initializes, and uninitializes noncre­ ateable object map entries. It does all that plus creates class objects and class in­ stances for createable object map entries. A class listed in the object map using the OB JECT_ENTRY一 NON一 CREATEABLE macro must provide the first three well-known static methods listed in Table 4.1. A class listed in the object map using the 0B3ECT_ENTRY macro must provide the same three methods as a noncreateable class plus the last three well-known static methods and two typedefs listed in Table 4.1. All classes listed in the object map must define an UpdateRegi s try method. It’s the only method not provided by any base class. As you,ll see soon, ATL contains various macros that expand to different implementations of this method, so the method is not difficult to provide. All ATL objects derive from CComObj ectRootBase and therefore already have a default implementation of the Ob j ectMai n method. Description ATL INTERNALS Table 4.1. Object Map Class Methods Static member function UpdateRegistry O b je ctM a in CetCategoryMap —CreatorC Iass:: Createlnstance _C1assFactoryCreatorCl ass:: Createlnstance GetObjectDescription Registers and unregisters the class. The DECLARE_REGISTRY macros provide various implementations of this method. Initializes and uninitializes the class. CComObjectRootBase provides a de­ fault implementation of this method. Returns a pointer to a component cate­ gory map. Use the BEGIN_CATEGORY_ MAP macros to provide a specialized im­ plementation. CComCoClass provides a default implementation of this method. The DECLAREJ\GGREGATABLE macros set the —CreatorCI ass typedef to the name of the class that creates instances. CComCoCl ass provides a default defini­ tion of this typedef. The DECLARE^CLASSFACTORY macros set the _C1assFactoryCreatorClass typedef to the name of the class that creates class objects. CComCoCl ass pro­ vides a default definition of this typedef. Returns the object description text string. CComCoCl ass provides a default imple­ mentation of this method. Most createable ATL classes derive from CComCoCl ass, which provides the im­ plementation of the remaining four methods required by an object map entry as well as both the required typedefs. A noncreateable class must implement the Cet- Cat ego ryMap method. ATL contains a set of macros that define and implement this method as well. Class Registration Support Methods When ATL registers all the classes in a server, ATL iterates over the object map look­ ing for entries containing a NULL GetObjectDescri p tio n function pointer and entries where the function returns a NULL description. The latter case is the typical COM SERVERS 165 one because CComCoCl ass provides the following implementation of C etO bject- Descri ptio n , and the object map stores a pointer to this method: static LPCTSTR WINAPI CetObjectDescriptionC) {return NULL;} For every such entry, ATL calls the UpdateRegi s t ry method to register or unreg- isler the class, then registers or unregisters the component categories for the class using the table of required and implemented categories provided by CetCategory- Map method. The GetObjectDescription Method The CetO bjectDescri p ti on static method retrieves the text description for your class object, as shown previously, the default implementation returns NULL. You can override this method with the DECLARE_OB〕ECT_DESCRIPTION macro. For example: cla s s CMyClass : p u b lic CComCoClass< • •• >, { p u b lic : DECLARE_OBJECT_DESCRIPTION(nMyClass O bject D e s c rip tio n ") When you ask a COM server to register its classes, it registers all of them. You can’t ask a server to register just one, or some subset, of its classes. A Component Registrar Object. To register or unregister a subset of the classes in a server, you need to provide a Component Registrar object. This is a COM cre­ ateable class that implements the IComponentRegi s tra r interface. [ ;: : ,■ g .. , object, dualf pointer一default(unique) uuid(a817e7a2-43fa-lld0-9e44-00aa00b6770a) ] : ::: , interface IComponentRegi strar : IDispatch { [id (l)] HRESULT Attach([in] BSTR bstrPath); [id(2)] HRESULT RegisterAllO; [id(3)] HRESULT UnregisterAll(); 166 ATL INTERNALS [ id ( 4 ) ] HRESULT CetComponentsC[out] SAFEARRAY(BSTR)^ pbstrCLSIOs, [o u t] SAFEARRAY(BSTR)* b s trD e s c rip tio n s ); [id (5 )] HRESULT RegisterComponent( [in ] BSTR bstrCLSIO); [id (6 )] HRESULT UnregisterComponentC[in] BSTR bstrCLSIO); A Component Registrar object registers all objects in your server that declare the DECLARE一OBJECT一DESCRIPTION macro. Using the Component Registrar object, you can register and unregister objects individually, unlike Dl 1 Regi sterS erver and D ll Unregi sterServer, which register and unregister all objects in your server. You may also get a list of objects in the server and their descriptions with the IComponentRegi s tra r: :GetComponents method. Normally, CetObject- Descri p ti on is called by IComponentRegi s t ra r:: CetComponents. When you create a Component Registrar object with the ATL Object Wizard, the wizard w ill automatically implement the IComponentRegi s tra r interface. It’s important to realize that when ATL registers all classes in a server, it does not register any class that provides a non-NULL object description. ATL expects some other software to use the Component Registrar object in the server to regis­ ter the classes with an object description. At one time, Microsoft Transaction Server (MTS) was going to use the Component Registrar object to register and un­ register individual classes in a server. However, I can find no references describing if, when, or how MTS uses the Component Registrar object Basically, as of this w rit­ ing, the Component Registrar object and class object descriptions seem to be un­ used features, and you shouldn’t use them. The UpdateRegistry Method Every class that you list in the object map, createable and noncreateable, must pro­ vide an UpdateRegi s try static member function. The ATL server implementation calls this method to ask the class to register and unregister itself depending on the value of bRegi ster. sta tic HRESULT WINAPI UpdateRegi stry(BOOL b R e g iste r); The DECLARE ^REGISTRY Macros. You can provide a custom implementation e f UpdateRegi s try (that is, write it yourself) or use an ATL-provided implementa­ tion. ATL provides an implementation of this method when you use one of the fol­ lowing macros in your class declaration: COM SERVERS 167 # d e fin e DECLARE_NO_REGISTRY()\ sta tic HRESULT WINAPI UpdateRegi stry (BOOL /*bR egisterV )\ {re tu rn S.OK;> #define DECLARE_RECISTRY(class, pidf vpid, nid, flags)\ sta tic HRESULT WINAPI UpdateRegistry(BOOL bRegister) {\ return _Module.UpdateRegistryClassCCetObjectCLSIDO, p1df \ vpid, nid, flags, bRegister);\ } # d e fin e DECLARE_RECISTRY_RESOURCE(x)\ sta tic HRESULT WINAPI UpdateRegistry(BOOL bRegister) {\ return _Hodule.UpdateRegistryFromResource(.T(#x), bRegister);\ } #de fin e DECLARE_RECISTRY_RESOURCEID(x)\ sta tic HRESULT WINAPI UpdateRegistryCBOOL bRegister) { \ return «Module.UpdateRegistryFromRcsource(x, bRegister);\ > The DECLARE_NO_RECISTRY macro provides an UpdateRegi s try method that simply returns S_OK. You commonly use this macro when declaring a non­ createable class. For example, here is the CSoapBox noncreateable object class declaration: BEGIN—OB] ECT_MAP(ObjectMap) 參 • • OBJ ECT^ENTRY_NON_CREATEAB LE(CSoapBox) END_0B3ECT_MAP() cla ss ATL_NO_VTABLE CSoapBox : public CComObjectRootEx, . . . / / N o CComCoClass d e riv a tio n { p u b lic : DECLARE_NO^RECISTRY() ATL INTERNALS The DECLARE一 REGISTRY macro provides an implementation that registers the standard class information for an object: the class, the path to the server module, the programmatic identifier (ProgID), the version-independent ProgID, the descrip­ tion, and the threading model. This macro uses the GetObjectCLSID function to obtain the CLSID to register. Normally, your CComCoCl ass base class provides the GetObjectCLSID implementation. The DECLARE_RECISTRY macro isn’t generally useful for modem components because many servers need more than the standard registry entries it provides. The preferred registration technique for ATL servers is to use registry scripts, which are far more flexible than DECLARE-REGISTRY. When asked to register o r un­ register, a server using registry scripts uses an interpreter object to parse the script and make the appropriate registry changes. The interpreter object implements the IRegi s tra r interface. ATL provides such an object, which can be either statically linked to reduce dependencies or dynamically loaded using CoCreatelnstance for the smallest code size. The DECLARE_RECISTRY_RESOURCE and DECLARE_REGISTRY_RESOURCEID macros provide an implementation of UpdateRegi s try that delegates the call to the CComModul e : : UpdateRegi stryF rom R esource method. You specify a string resource name when you use the first macro. The second macro expects an integer resource identifier. The UpdateRegi stryFromResource runs the script con­ tained in the specified resource. When bRegi s te r is TRUE, this method adds the script entries to the system registry; otherwise, it removes the entries. Registry Script Files. Registry scripts are text files that specify what registry changes must be made for a given CLSID. Wizard-generated code uses an RGS ex­ tension by default for registry script files. Your server contains the script file as a custom resource of type REGISTRY in your executable or DLL. Registry script syntax isn't complicated, and can be summarized as follows: [NoRemove 丨 ForceRenove I val] Name | [ » s *Value* | d •V alue’ I b •V a lu e .] { … optional script entries for subkeys The NoRemove prefix specifies that the parser should not remove the key when un­ registering. The ForceRemove prefix specifies that the parser should remijve the current key and any sub keys prior to writing the key. The val prefix specifies that the entry is a named value and not a key. The s, d,and b value prefixes indicate REG_SZ, REG_DWORD, and REC_BINARY, respectively. The Name token is the string COM SERVERS for the named value or key. It must be surrounded by apostrophes when the string contains spaces; otherwise, the apostrophes are optional. ATL’s parser recognizes the standard registry key names, for example, HKEY_CLASSES_ROOT, as well as their four-character abbreviations (HKCR). As an example demonstrating the flexibility (and improved readability) of registry scripts, I present two techniques for registering a class. First, here’s a REGEDIT4 sample for the nontrivial Demagogue class registration. Watch out, because a few lines are too long to list on the page and have wrapped. RE0tDIT4 [HKEY_CLASSES_R00T\ATLInterna1s•Demagogue•1] Demagogue C lass" [HKEY_CLASSES_ROOT\ATLInterna1s•Demagogue. 1\CLSID] @-,,{95CD3731-FC5C-llDl-8CCB-00A0C9C8E50D}M [HKEY_CLASSES_ROOT\ATLInterna1s•Demagogue] @="Demagogue C lass" [HKEY_CLASSES_R00T\ATLInterna1s.Demagogue\CLSID] @«"{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}" [HKEV_CLASSES_R00T\ATLInternals.Demagogue\CurVer3 @=”ATLInterna1s•Demagogue•1" [HKEY_CLASSES^R0OT\CLSID\{95CD3731-FC5C-llDl-8CC3-00A0C9C8E50D}] Demagogue C lass" [HKEY_CLASSES_R00T\CLSID\{95CD3731-FC5C-llDl-8CC3-00A0C9C8E50D}\ProgID] @ ="A TLInternals. Demagogue.1" [HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-llDl-8CCB- 00A0C9C8E50D}\VersionIndependentProgID] (a="ATLInternals .Demagogue" [HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-llDl-8CCB- 00A0C9C8E50D}\Programmable] [HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-llDl-8CC3- 00A0C9C8E50D}\InprocServer32] :\\ATLINT-l\\Debug\\ATLINT~l.DLL" "Threadi ngModel"-.Apartment" [HKEY_CLASSES_ROOT\\CLSID\\{95CD3731-FC5C-llDl-8CC3- 00A0C9C8E50D}\TypeLib] @="{95CD3721-FCSC-11D1-8CC3-00A0C9C8E50D}” [HKEY„CLASSES_ROOT\CLSID\{95CDB731-FC5C-11D1-8CC3- 00A0C9C8E50D}\Implemented Categories] [HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-llDl-8CC3- 00A0C9C8E50D}\Imp1emented Categories\{0D22FF22-28CC-llD2-ABDD- 00A0C9C8E500}] ATL INTERNALS The corresponding registry script would look like this: HKCR { A T L In te rn a ls . Demagogue.1 = s 'Demagogue C la s s ’ { CLSID = s 1{95CDB731-FC5C-11D1-8CC3-O0AOC9C8E5OD}' } A T L In te rn a ls . Demagogue = s ' Demagogue C lass' { CLSID = s 1{95CD3731-FC5C-11D1-8CC3-O0AOC9C8E5OD}1 CurVer = s * A T L In te rn a ls . Demagogue.1 ' } NoRemove CLSID { ForceRemove {95CD3731-FC5C-11D1-8CC3-0OAOC9C8E5OD} = s 'Demagogue C lass' { ProgID = s ' A T L In te rn a ls . Demagogue.1 ' VersionlndependentProgXD = s ' ATLInternals.Demagogue' ForceRemove 'Programmable' InprocServer32 - s ,%M0DULE%' { val ThreadingModel = s * Apartment' } •TypeLib' = s ' {95CD3721-FC5C-11D1-8CC3-00A0C9C8E50D}' 'Implemented Categories' { {OD22FF22-28CC-11D2-ABDD-00AOC9C8E5OD} After you have the resource script file, you reference the file in your server’s re­ source (• rc) file. You can either reference it using an integer identifier or a string identifier. In a typical ATL project, each class can have a registry script file, and the server as a whole typically has its own unique registry script file. In the following examples, the Demagogue script file uses the integer identifier IDR_DEMACOCUE, whereas the EarPolitic script file uses EARPOLITIC as its string identifier. ATL wizard-created classes use the DECLARE_RECISTRY_RESOURCEID COM SERVERS 171 macro to specify a resource by its integer identifier. You can use the DECLARE_ REGISTRY—RESOURCE macro to identify a resource by its name. // resource.h file # d e fin e IDR^DEMACOGUE 102 // Server.rc file IDR一DEMAGOGUE REGISTRY DISCARDABLE "Demagogue. rg s ” EARPOLITIC REGISTRY DISCARDABLE " E a r P o litic . rg s u / / Demagogue.h f i l e c la s s ATI__N0_VTABLE CDemagogue : public CComObjectRootEx, p u b lic CComCoClass, p u b lic : DECLARE_RECISTRY_RESOURCEIDCIDR_DEMACOCUE) // EarPolitic.h file c la s s ATL_NO_VTABLE C E a r P o litic : publi c CComObjectRootEx, public CComCoClass, publ i c : DECLARE_RECISTRY_RE50URCE(EARP0LITIC) Registry Script Variables. Note that in the registry script one of the lines refer­ enced a symbol called %M0Dl)LE%: InprocServer32 = s ’^D U LE t When the parser evaluates the script, it replaces all occurrences of the registry script variable %M0DULE% with the actual results of a call to CetModul eFi ^ eName. So what the parser actually registered looked like this: InprocServer32 = s 'C:WATLInternals\\Debug\\ATLInternals.d ll' 172 ATL INTERNALS You can use additional, custom registry script variables in your scripts. Your server must provide the registry script parser with a table that maps variable names to replacement values when you ask the parser to parse the script. The parser w ill substitute the replacement values for the variable names prior to registration. To use custom registry script variables, first select the registry script variable name, using percent signs to delimit the name. Here is a sample line from a registry script: D a te ln s ta lle d * s '%INSTALLDATE%' Then, instead o f using the DECLARE_REGISTRY_RESOURCEID macro, define a cus­ tom UpdateRegi s try method. In the method, build a table of replacement name/ value pairs. Finally, call the CComModule::UpdateRegistryFromResource method specifying the resource identifier, a register/unregister flag,and a pointer to the replacement name/value table. Note that ATL uses the provided table entries in addition to the default replacement map (which, as of this writing, only contains %MODULE%). Here is an example from the Demagogue class that substitutes the variable %INSTALLDATE% with a string containing the current date: sta tic HRESULT WINAPI UpdateRegistry(BOOL b) { OLECHAR wszDate [1 6 ]; SYSTEMTIME s t; CetLocalTi me(&st); wsprintfW(wszDate, LM%.4d/%.2d/%.2dM, st.wYear, st.wMonth, st.wOay); «ATL_REGMAP_ENTRY rm [] « { { OLESTR(,,INSTALLDATE,,) ( wszDate}, { 0, 0 } } ; return _Module.UpdateRegistryFromResource(IDR_OEMACOGUE, b, rm); } After registration of the class, the registry key D atelnstalled will contain the year, month, and day, in the form “yyyy/mm/dd,” at the time of install. The GetCategoryMap Method The last step in the registration process for each class in the object map is to regis­ ter the component categories for the class. The ATL server registration code calls each class’s GetCategoryMap method to ask the class for its list of required and im­ plemented component categories. The method looks like this: s t a t ic const s tru c t ««ATL_CATMAP—ENTRY* CetCategoryMapO { re tu rn NULL; } COM SERVERS 173 The ATL server registration code uses the standard component category manager object (CLSID一StdComponentCategoriesMgr) to register your class’s required and implemented categories. Older versions of Win32 operating systems do not have this component installed. When the category manager is not installed, your class’s registration of its component categories silently fails. Typically, this is not good. Microsoft permits you to redistribute the standard component category man­ ager (com cat.dll) with your application. However, it’s really easier simply to define the component categories in your class’s registry script file. As it turns out, you might want to use the script file anyway because, at the time of this writing, ATL 3.0 has a bug involving the category map and noncreateable classes. I’ll discuss the bug shortly. The Component Category Map Typically, you w ill use ATL-provided category map macros for the implementation of this method. Here’s a typical category map: / / {OD22FF22-28CC-lld2-ABDO-00A0C9C8E50D} s t a t ic const CUID CATID_ATLINTERNALS_SAMPLES - {0xd22ff22, 0x28cc, 0xlld2, {Oxab, Oxdd, 0x0, OxaO, 0xc9, 0xc8f 0xe5, Oxd}}; BECIN.CATECORY_MAP(CDemagogue) IMPLEMENTED.CATECORY(CATID^ATLINTERNALS_SAHPLES) END.CATECORYJ1AP() This example defines a component category called CATID_ATLINTERNALS_ SAMPLES. All examples in this book register themselves as a member of this category. The Category Map Macros. The BECIN_CATEGORY_MAP macro declares the GetCat ego ryMap static member function, which returns a pointer to an array of -ATL_CATMAP_ENTRY entries, each o f which describes one component category that is either required or implemented by your class. # d e fin e BECIN.CATECORY_MAP(x)\ sta tic const struct _ATL_CATMAP.ENTRY* CetCategoryMap() {\ static const struct JMUCATMAP.ENTRr pHap[] • { The IMPLEMENTED^CATEGORY and REQUIRED—CATEGORY macros populate the table with the appropriate entries. ATL INTERNALS ^ d e fin e IMPLEMENTED_CATECORY(catid)\ { JML.CATMAP.ENTRY.IMPLEMENTED, A c a tid }, # d e fin e REQUIRED一CATECORY(catid)\ { _ATL_CATMAP_ENTRY_REQUIRED, Acatid }, The END_CATECORY_MAP adds a delim iting entry to the table and completes the CetCategoryMap function. ^define END_CATECORY_MAP()\ { J\TL-CATMAP_ENTRY_END. NULL > } ;\ return( pfiap ); } Each table entry contains a flag (indicating whether the entry describes a re­ quired category, an implemented category, or is the end-of-table entry) and a pointer to the category identifier (CAT1D). struct JVTL_CATMAP_ENTRY { int iType; const CATID* pcatid; 参d e fin e JMUCATMAP_ENTRY_END 0 参define _ATL.CATMAP_ENTRY_IMPLEMENTED 1 参define ^ATL^CATMAP.ENTRY.REQUIRED 2 The ATLhelper function A tl RegisterClassCategoriesHel per iterates through the table and uses COM’s standard component category manager to register each CATID as a required or implemented category for your class. The ATL server regis­ tration code uses this helper function to register the component categories for each class in the object map. Unfortunately, the category map logic does not add a category to a system be­ fore trying to enroll a class as a member of the (nonexistent) category. The compo­ nent category manager silently fails to add a category to a class when the category isn’t already defined on the system. This means that you must enhance the registration of the server itself so that it registers any custom component categories used by your classes. For example, the following registry script registers the application identifier (AppID) for an inproc server and it also adds a component category to the list of component categories on the system. COM SERVERS 175 HKCR { NoRemove AppID { {A11552A2-28DF-lld2-ABDD-OOAOC9C8E50D} = s 'ATLInternals *ATLInterna1s.DLL' { val AppID = s {A11552A2-28DF-lld2-ABDD-00A0C9C8E50D> } } NoRemove 'Component Categories' { {0D22FF22-28CC-lld2-ABDD-00A0C9C8E50D} { val 409 = s ’ATL Internals Example Components' } } } This technique defines all categories (just one in the above example) used by the classes implemented in this server in the system registry. Separately, each class w ill register itself as a member of one or more of these categories. Category Map Bug for Noncreateable Classes. You must supply an empty cate­ gory map to be able to add a noncreateable class to the object map at all using the OB〕ECT_ENTRY_NON—CREATEABLE macro. Once you have the empty category map in place, you might want to add a few implemented or required categories to the noncreateable class’s map. The ATL 3.0 implementation of the category map pro­ cessing has a bug when you have a nonempty category map in a noncreateable class. The problem is that the category map registration code registers the catego­ ries in the map using information from the object map entry for the class contain­ ing the category map, specifically, the CLSID entry. Unfortunately, you cannot specify a CLSID using the OBJECT—ENTRY一NON_CREATEABLE macro. The macro sets the CLSID to GUID_NULL. Thus, all implemented and required categories in a noncreateable class’s category map end up registered under the HKCR/CLSID/ {GUID 一 NULL} registry key rather than the correct HKCR/CLSID/{Your CLSID}. 176 ATL INTERNALS You can easily work around this bug, however. I’ve defined a new macro, OBJ ECT_ENTRY_NON_CREATEABLE_EX. It looks like this: # d e fin e OB]ECT_ENTRY_NON_CREATEABLE_EX(clsid, c la s s ) \ {Aclsid, class::UpdateRegistry, \ NULL, NULL, NULL, 0, NULL, class::CetCategoryMap, class::ObjectMain }, You should use this version of the macro in preference to the one ATL provides un­ til the bug is fixed. In the meantime, I expect GUID一 NULL to join a lot of component categories. Server, not Class, Registration Often, you need registry entries for the server (inproc, local, or service) as a whole. For example, the HKCR/AppID registry entries for a server apply to the entire DLL or EXE, and not to a specific class implemented in the DLL or EXE. As mentioned previously, you have to register a new component category with the system before you can enroll a class as a member of the category. Up till now, we’ve seen only sup­ port for registration of class-specific information. This is simple enough. Write a registry script that adds the entries required by the server, such as its AppID and component categories used by the server's classes. Then register the server’s script before registering all the classes implemented in the server. The wizard-generated code for a local server and a service does this for you. Unfortunately, at present, the wizard-generated code for an inproc server does not. Local Server and Service Registration The ATL wizard-generated code for a local server creates a registry script file for your server registration in addition to any registry script files for your classes. By default, the server registry script defines only an AppID for the local server. Here are the entries for an ATL project called ATLLocal. // In the resource.h file #define IDRJVTLLocal 100 // In the ATLLocal.re file IDR^ATLLocal REGISTRY nATLLocal. rgs'* // ATLLocal.rgs file HKCR { COM SERVERS 177 NoRemove AppID { {0068S9E6-2EF2-11D2-ABEA-00AOC9C8E50D} ■ s ’ ATLLocaV 'ATLLocal.EXEf { val AppID « s {0068S9E6-2EF2-11D2-ABEA-00A0C9C8E50D} } } } A wizard-generated local server’s Wi nMai n code registers or unregisters the server's resource script entries, then calls the _Module object’s Regi s te 「Server method to do the same for each class in the object map. if (1strcmpi(1pszToken, _T(MUnregServer"))««0) { ..Module•UpdateRegistryFromResource(IOR_ATLLocal, FALSE); nRet * _Modu1e.UnregisterServer(TRUE); 參 •參 break; } i f (1 strcmpi(1 pszToken, _T("RegServer"))—0) { _Module.UpdateRegistryFromResource(IDR_ATLLocal, TRUE); nRet = —Module.RegisterServer(TRUE); 參 •參 break; > A service specializes the CComModul e class with a derivation called CSer- viceModule. CServiceModule provides an overloaded RegisterServer method, which performs server registration before calling CComModul e :: Regi s- te 「Server to register the classes in the object map. in lin e HRESULT CServiceModule::RegisterServer(BOOL bRegTypeLib, BOOL bService) { HRESULT hr - Colnitialize(NULL); if (FAILEO(hr)) return hr; / / Add s e rv ic e e n trie s UpdateRegi s t ryFromResource(IDR_JATLServi ce, TRUE); 178 ATL INTERNALS II Add object entries hr • CComModu1e::Regi sterServer(bRegTypeLi b); CoUninitializeC); re tu rn h r; > Inproc Server Registration Unfortunately, the wizard-generated code for an inproc server doesn’t provide equivalent code, even though an inproc server may need an AppID key, as well as other server-specific, rather than class-specific, registration. Fortunately, it’s easy to provide this functionality for an inproc server. II In the resource.h file # d e fin e IDR_ATLInProc 100 // In the ATLInProc.rc file IDR_ATLInProc REGISTRY "ATLInProc.rgs" // ATLInProc.rgs file HKCR { NoRemove AppID { {A11552A2-28DF-lld2-ABDD-(>0A0C9C8E50D} = s ’ATLInProc• 'ATLInProc.DLL' { val AppID = s {AU552A2-28DF-lld2-ABDD-OOAOC9C8E50D} You then need to add an UpdateRegi stryFromResource function call to the Dl 1 Regi sterS erver and Dl 1 Unregi sterS erver methods of the inproc server. ///////////////////////////////////////////// // Dl1RegisterServer-Adds entries to the system registry COM SERVERS STDAPI DllRegisterServer(void) { HRESULT h r = .Module.UpdateRegistryFromResource(IDR_ATLInProc, TRUE); if (FAILED (hr)) return hr ; // registers object, type!ib, and all interfaces in typelib return .Module.RegisterServer(TRUE); ///////////////////////////////////////////// // Dl1Unregi sterServer-Removes entries from the system regi stry STDAPI Dl1UnregisterServer(void) { HRESULT h r = .Module.UpdateRegistryFromResourceCIDR_ATLInProc, FALSE); if (FAILED (hr)) return hr ; return _Module.UnregisterServer(TRUE); } Class Initialization and Uninitialization The ObjectMain Method Typically, in an ATL project, you don't want to use global or static objects for class- level initialization because the C runtime library (CRT) calls the constructors of static and global objects. Linking with the CRT increases the size of your server con­ siderably for such little benefit. ATL supports class-level initialization for all classes listed in the object map, createable and noncreateable. In fact, frequently you’ll add a noncreateable class entry to the object map solely because the noncreateable class needs class-level initialization. (A createable class must always be in the ob­ ject map and therefore w ill always receive class-level initialization.) When an ATL server initializes, it iterates over the object map and calls the Ob- jectM ai n static member function (with the b S ta rti ng parameter set to tru e ) of each class in the map. When an ATL server terminates, it calls the ObjectMain function (with the b S ta rti ng parameter set to fa ls e ) of each class in the map. You always have a default implementation, provided by CComObj ectRootBase, that does nothing and looks like this: s t a t ic v o id WINAPI O bjectH ain(bool b S ta rtin g ) { } ; 180 ATL INTERNALS When you have class initialization and termination logic (as opposed to in ­ stance initialization and termination logic), define an ObjectMai n static member function in your class and place the logic there.6 BEGIN—0B;)ECT_MAP(0bjectMap) • 參 _ OBJECT_ENTRY_NON^CREATEABLE(CSoapBox) END_0B3ECT_MAP() cla ss ATL_N0_VTABLE CSoapBox : public CComObjectRootEx, p u b lic : DECLARE_N0—REGISTRYO • • • s ta t ic v o id WINAPI O bjectM ain(bool b S t a r t in g ) ; }; Instantiation Requests Class Object Registration ATL creates the class objects for an inproc server slightly differently than it does the class objects for a local server or service. For an inproc server, ATL defers creating each class object until the SCM actually requests the class object via D1 lC e t- C1 assOb je c t. For a local server or service, ATL creates all class objects during server initialization, then registers the objects with the SCM. For an inproc server, ATL uses the A tl ModuleGetCl assOb j e ct helper func­ tion to scan the object map, create, if necessary, the class object, and return the re­ quested interface on the class object ATLINLINE ATLAPI A t1Modu1eGetClas sObj e c t(JVTL^IOOULE* pM, REFCLSID r c ls id , REFIID r i i d , LPVOID* ppv) { • 擎 ♦ _ATL.0B3MAP^ENTRY* pE ntry - pM->nupObjMap; •Instance initialization and termination logic should go in the Final Const ru ct and Final Release methods, as Chapter 3 describes. COM SERVERS while (pEntry->pclsid !■ NULL) { if ((pEntry->pfnCetC1assObject !* NULL) && InlinelsEqualCUIDCrclsid, *pEntry->pclsid)) { i f (pEntry->pCF — NULL) { EnterCriticalSection(&pM->m_csObjMap); if (pEntry->pCF *- NULL) hRes ■ pEntry->pfnCetClassObject( pEntry->pfnCreateInstance, IID 一IUnknown, (LPVOID*)&pEntry->pCF); • LeaveCriticalSection(&pM->m_csObjMap); > , . i f (pEntry->pCF !■ NULL) hRes ■ pEntry->pCF->QueryInterface(r1id, ppv); break; } pEntry ■ _NextObjectMapEntry(pM, pEntry); > i f (*ppv «■ NULL && hRes * * S一 OK) hRes ■ CLASS^E„CLASSNOTAVAILABLE; return hRes; } Notice how the logic checks to see if the class object has not already been created (p E n try-> pC F == NULL), then acquires the critical section that guards access to the object map, then checks once more that pCF is still NULL. What might not be obvious is why ATL checks twice. This is to maximize concurrency by avoiding grabbing the critical section in the normal case (when the class factory has already been created). Also notice that ATL caches the IUnknown interface for the class object in the object map entry’s pCF member variable. Because the SCM may well make subse­ quent requests for the same class object, ATL caches the IUnknown pointer to the object in the pCF field of the object map entry for the class. Subsequent requests for the same class object reuse the cached interface pointer. There is a helper method, called GetCI as sObj ect, in your _Modul e global ob­ ject (discussed in detail later in this chapter) that you can use to retrieve a previ­ ously registered class object. However, it only works for inproc servers. I I O btain a Class Factory (DLL o n ly ) HRESULT GetClassObject(REFCLSID rclsid , REFIID riid , LPVOID* ppv); 182 ATL INTERNALS You would use it like this: IClassFactory* pFactory; HRESULT hr = —Module.CetClassObject (CLSID_Demagogue, IID_IC1assFactory reinterpret一cast (&pFactory); A local server must register all its class objects during the server’s initialization. ATL uses the AtlModuleRegi sterCIassO bjects helper function to register all the class objects in a local server. This helper function iterates over the object map, calling Regi sterC I assOb je c t for each object map entry. ATLINLINE ATLAPI A tlM oduleR egisterC lassO bjects CATUMODULE* pM, OWORO dwClsContextr DWORD dwFlags) { • • • ^TL-OBJMAP^ENTRY* pEntry 藤 pM->nupObjMap; HRESULT hRes ■ SJOK; w h ile (p E n try -> p c ls id !» NULL && hRes S.OK) { hRes « pEntry->Regi sterCIassObject(dwClsContext, dwFlags); pEntry * _NextObjectMapEntry(pMf pEntry); > return hRes; > ■ | Regi sterC I assOb je c t is a method of the _ATL_OB]MAP_ENTRY structure. It ba­ sically encapsulates the process of creating, then registering a class object from an object map entry. First, it ignores entries with a NULL pfnCetCl assO bject func­ tion pointer. This skips the noncreateable class entries in the map. Then, Regi s- te rC l assO bject creates the instance of the class object and registers it. s tr u c t _ATUOBJMAP一 ENTRY { HRESULT WINAPI RegisterCIassObject(DWORD dwClsContext. DWORD dwFlags) { IUnknown* p « NULL; i f CpfndetCIassObject *« NULL) return S_OK; HRESULT hRes • pfnCetClassObject(pfnCreatelnstance, IIDJUnknown, (LPVOID*) 4p); 1 f CSUCCEEOEO(hRes)) H hRes _ CoRegisterCIassObjectC*pc1sid, p , COM SERVERS 183 dwClsContext, dwFlags, AdwRegister); i f (p !■ NULL) p->R e le a se (); return hRes; Notice that the function does not cache the IUnknown interface pointer to the class object it creates. It hands the interface to the SCM (which caches the pointer), stores the registration code in the object map, and releases the interface pointer. Because ATL does not cache any class object interface pointers in an out-of-process server, the simplest method for obtaining your own class object from within the server is to ask the SCM for it by calling CoGetCl assOb je c t. The _ClassFactoryCreatorClass and _CreatorClass typedefs When ATL created a class object in the prior examples, it asked your class to in­ stantiate the class object by calling indirectly through the function pointer pfnC et- C1 assOb j ect, which ATL stores in the object map entry for the class. s tru c t JVTL.OB3MAP^ENTRY { • • • «ATL.CREATORFUNC* pfnCetClassObject; / / Creates a class object «ATU.CR£ATORRJNC* pfnCreatelnstance; / / Creates a class instance This member variable is of type __ATL_CREATORFUNC* and is a creator function.7 Notice that the pfnC reatelnstance member variable is also a creator function pointer. typedef HRESULT (WINAPI J^L-CREATORFUNC) (voi d* pv, REFIID riid, LPVOIO* ppv); These function pointers are non-NULL only when you describe a COM createable class using the OBJ ECT_ENTRY macro. 7 Although there are logically two types of creator functions, instance creator functions and class creator functions, all creator functions used by the object map have a static member function with the example function signature. Chapter 3 discusses the various types , A createable class must define a typedef, called _C1 assFactoryC reator- C1 ass, which ATL uses as the name of the class object's creator class. The OBJ ECT— ENTRY macro expects this creator class to have a static member function called C reatelnstance and stores the address of this static member function in the pfnCetCI assObj ect object map entry. A createable class also must define a typedef, called 一 CreatorCl ass, which ATL uses as the name of the class instance’s creator class. The OB JECT一 ENTRY macro expects this creator class to have a static member function called C reate­ lnstance and stores the address of this static member function in the pfnCre­ atelnstance object map entry. DECLARE一CLASSFACTORY Typically, your createable class w ill inherit a definition of the _C1 assFactory- CreatorCl ass typedef from its CComCoClass base class. CComCoClass uses the DECLARE_CLASSFACTORY macro to define an 叩 propriate default class object creator class, based on the type of server. tem plate 〈cla ss T, const CLSID* p c ls id ■ ACLSI0_NULL> c la s s CComCoClass p u b lic : OECLARICUSSFACTORY () DECLARE.ACCRECATA8LE(T) The _ClassFactoryCreatorClass typedef The DECLARE一 CLASS FACTORY macro evaluates to the DECLARE_CLASSFACTORY 一 EX macro w ith CComCl ass F a c to ry as the c f argum ent The DECLARE_CLASS- FACTORY一 EX macro produces a typedef for the symbol „C1 assFactory- C reatorC l ass. This typede f is the name of a creator class that ATL uses to create the c l丄ss object fo r a class. COM SERVERS fd e fin e OECLARE_CLASSFACTORY() \ DECLARE-CLASSFACTORY—EX CCComClassFactory) #1f definedCWINOLL) 丨 d e fin e d ( 一USRDU) # d e fin e DECLAREJILASSFACTORY一E X (cf) \ typedef CComCreator< CComObjectCached< c f > > \ .ClassFactoryCreatorClass; 秦 else // don't let class factory refcount influence lock count # d e fin e DECLARL_CLASSFACTORY_EX(cf) \ typedef CComCreator< CComObjectNolock< cf > > \ .ClassFactoryCreatorClass; # e n d if When you build an inproc server, ATL standard build options define the -USRDLL preprocessor symbol. This causes the _C1 assFactoryC reatorC l ass typedef to evaluate to a CComCreator class that creates a CComObject- Cached version of your class object class c f. Out-of-process servers evaluate the typedef as a CComCreator class that creates a CComObj ectNoLock ver­ sion of the class object class c f. Class Object Usage of CComObjectCached and CComObjectNoLock As described in Chapter 3,the CComObj ectCached:: AddRef method does not in­ crement the server’s lock count until the cached object’s reference count changes from 1 to 2. Similarly, Release doesn’t decrement the server’s lock count until the cached object’s reference count changes from 2 to 1. ATL caches the IUnknown interface pointer to an inproc server’s class object in the object map. This cached interface pointer represents a reference. If this cached reference affected the server’s lock count, the DLL could not unload until the server released this interface pointer. However, the server doesn’t release the interface pointer until the server is unloading. In other words, the server would never unload. By waiting to acyust the server’s reference count until there is a second reference to the class object, the reference in the object map isn’t sufficient to keep the DLL loaded. Also described in Chapter 3,CComObjectNoLock never ac^justs the server's lock count. This means an instance of CComObjectNoLock does not keep a server loaded. This is exactly what we need for a class object in a local server. When ATL creates a class object for an out-of-process server, it registers the class object with the SCM, then ATL releases its reference. However, the SCM w ill keep an unknown number of references to the class object, where “unknown” 186 ATL INTERNALS means one or more. Therefore, the CComObj ectCached class won’t work correctly for an out-of-process class object. ATL uses the CComObj ectNoLock class for out- of-process class objects because references to such objects don’t affect the server’s lifetime in any way. However, in modem versions of COM, the marshaling stub w ill call the class object’s LockSe rve r method when it marshals an interface pointer to a remote client This keeps the server loaded when out-of-apartment clients have a reference to the class object Class Object Instantiation 一 CComCreator: :Createlnstance Revisited Earlier in this chapter, you saw the ATL class object registration helper functions A tl ModuleGetCl assObject and Regi sterC I assOb j ect. When they create a class object, they call the class object’s creator class’s C reatelnstance method like this: // Inproc server class object instantiation I ATLINLINE ATLAPI AtlHoduleCetClassObject(«ATL_MOOULE* pM, REFCLSID r c ls id , REFIID r i i d , LPVOIO* ppv) { • • • hRes ■ pEntry->pfnCetClassObject(pEntry->pfnCreateInstance, IID 一 IUnknown, (LPV0ID*)4pEntry->pCF); // Out-of-process server class object instantiation s tr u c t JMU.0B3MAP^ENTRY { HRESULT WINAPI Regi sterCIassObject(DWORD dwClsContext, OWORO dwFlags) { 錢 HRESULT hRes • pfnCetClassObject(pfnCreatelnstance, IID_IUnknown, (LPVOIO*) &p); Recall that the pfnG etClassO bject member variable is set by the OBJECT一 ENTRY macro and contains a pointer to the Createlnstance method of the —Class FactoryCreatorClass. COM SERVERS 187 class::.ClassFactoryCreatorClass::CreateInstance For an inproc server, this evaluates to the following (assuming you’re using the default class factory class, which is CComCl assFactory): c la s s : :CC > e a to r< CCo > : :Creat__ stance For an out-of-process server, this evaluates to c la s s ::CComCreator< CComObjectNoLock< CComClassFactory > > : :CreateInstance This means the p f nCetCl assObj ect member points to the Createlnstance method of the appropriate parameterized CComCreator class. When ATL calls this Createlnstance method, the creator class creates the appropriate type of the CComCl ass Facto ry instance (cached or no lock) for the server. You saw the definition of the CComCreator class in Chapter 3, but let’s examine part of the code in depth. template class CComCreator { p u b lic : s t a t ic HRESULT WINAPI C re a te In s ta n c e (v o id * pv, After the creator class’s Createlnstance method creates the appropriate class object, the method calls the class object’s SetVoid method, passing it the pv parameter of the C reatelnstance call. Note that ATL uses a creator class both to create instances of class objects (sometimes called class fa c to r i es) and to REFIID r i i d . LPVOID* ppv) ATLTRY(p ■ new T l(p v )) i f (p !» NULL) { p->S€tVoid(pv); return hRes; 188 ATL INTERNALS create instances of a class (often called COM objects). For regular ol’ COM objects, ATL defines the SetVoid method in CComObj ectRootBase as a do-nothing method, so this creator class call to SetVoi d has no effect on an instance of a class. class CComObjectRootBase void SetVoid(vo1d*) {> However, when a creator class creates an instance of a class object, it’s typically creating a CComCl assFactory instance. CComCl assFactory overrides the S et- Void method. When a creator class calls the SetVoi d method while creating a class factory instance, the method saves the pv parameter in its m_pf nCreatelnstance member variable. class CComClassFactory : void SetVoid(void* pv) { «L_pfnCreateInstance - (_ATL_CREATORFUNC*)pv;} .ATL.CREATORFUNC* m ^pfnCreatelnstance; Let’s look at the class object creation code in the ATL helper function once again: HRESULT hRes - pfnC€tC1ass0bject( pfnCreatelnstance, The p f nCetCl assObj ect variable points to the Createlnstance creator func­ tion that creates the appropriate instance of the class object for the server. The pfnCreatelnstance variable points to the Createlnstance creator function that creates an instance of the class. When ATL calls the pfnCetCl assObj ect function, it passes the pfnC reate­ lnstance object map entry member variable as the pv parameter to the CCom­ Cl assFactory: :Createlnstance method. The class object saves this pointer public IIDJUnknown, (LPVOID*) Ap); COM SERVERS 189 in its m„pfnCreateInstance member variable and calls the m_pfnCreate- Instance function whenever a client requests the class object to create an in­ stance of the class. Whew! You must be wondering why ATL goes to all this trouble. Holding a function pointer to an instance creation function increases the size of every class object by_ • four bytes (the size of the m一 pfnCreatelnstance variable). Nearly everywhere else, ATL uses templates for this kind of feature. For example, we could define the CComCl ass Factory class to accept a template parameter that is the instance class to create. Then each instance of the class object wouldn't need the extra four-byte function pointer. The code would look something like this: template class CComClassFactory : STDMETHODCCreatelnstance)(LPUNKNOWN pUnkOuter, REFIID riid , void** ppvObj) ATLTRY(p = new T ( ) ) ; • • • } }; The problem w^th this alternative approach is that it actually takes more memory to implement. F、 ^xample, let’s assume you have a server that implements three classes: A, B, a. ATL’s currei. •仲 roach takes 12 bytes per class object (4-byte vptr, 4-byte un­ used reference co^vnt^and 4-byte function pointer) times three class objects (A, B, and C) plus 20 byti^s a single CComCl ass Factory vtable (five entries of 4 bytes each). All three classes' objects are actually unique instances of the same CCom­ Cl assFactory class, so all three share a single vtable. Each instance maintains unique state一 a function pointer—which creates different instance classes when called. This is a total of 56 bytes for the three class objects. (Recall that ATL never creates more than one instance of a class object.) The template approach takes 8 bytes per class object (4-byte vptr and 4-byte unused reference count) times three class objects (A, B,and C) plus 20 bytes each for three CComClassFactory vtables (one for CComCl ass Facto ry, one for CComCl assFactory, and one for CComCl assFactory). In this case, the class object doesn’t maintain state to tell it what instance class to create. Therefore, 190 ATL INTERNALS each class must have its own unique vtable that points to a unique Create­ ln sta n ce method that calls new on the appropriate class. This is a total of 84 bytes. The above is mostly a theoretical calculation, though. Most heap managers round allocations up to a multiple of 16 bytes, which makes the instance sizes the same. This more or less makes moot the issue that class objects carry around a ref­ erence count member variable that they never use. However, the memory savings are real, mainly due to the single required vtable. The function pointer implementation requires only a single copy of the IC1 ass­ Factory methods, therefore only one vtable, regardless of the number of classes implemented by a server. The template approach requires one copy of the IC1 ass­ Factory methods per class, therefore one vtable per class. The memory savings in­ crease as you have more classes in a server. I know. That’s more detail than you wanted to know. Think of all the character and moral fiber you’re building. You’re welcome. That’s why I’m here. Now, let’s look at how CComCl assFactory and related classes actually work. CComClassFactory and Friends DECLARE_CLASSFACTORY and CComClassFactory. Typically, ATL objects ac­ quire a class factory by deriving from CComCoCl ass. This class includes the macro DECLARE_CLASSFACTORY, which declares CComCl a s s F a c to ry as the default class factory class. The CComCl assFactory class is the most frequently used of the ATL-supplied class object implementations. It implements the IC lassFactory interface and also explicitly specifies that the class object needs the same level of thread safety, CComGl obal sTh readModel, that globally available objects require. This is be­ cause a class object can be accessed by multiple threads when the server's thread­ ing model is Apartment, Free, or Both. Only when the served r iding model is Single does the class object not need to be thread safe. public: 8ECIN_C0K-MAP(CComClassFactory) C0H_INTERFACE„ENTRY(IC1assFactory) END.C0M_MAP() // IClassFactory STDMETHOO(Createlnstance)(LPUNKNOWN pUnkOuter, class CComClassFactory : public IClassFactory, public CComObjectRootEx As described earlier, the object map entry for your class contains the original value for this function pointer. It points to an instance creator class, as described in Chap­ ter 3,and is set by the following part of the OB] ECT一ENTRY macro: class::_CreatorClass::CreateInstance, NULL, 0, \ When your class derives from CComCoClass, you inherit a typedef for _Cre- atorC I ass, which w ill be used unless you override it with a new definition. tem plate cla s s CComCoClass { p u b lic : OECLAREJVCCRECATABLE(T) The default DECLARE^ACGREGATABLE macro defines the C reatorC l ass typ e ­ def to reference the CComCreator2 class. This creator class creates instances of your class using one or two other creator classes, depending on whether or not the instance is aggregated. It uses a CComCreator to create instances of ATL INTERNALS CComObject type when asked to create a nonaggregated instance. It uses a CComCreator to create instances of CComAggObject type when asked to create an aggregated instance. # d e fin e DECLARE^ACCRECATABLE(x) p u b lic :\ typedef CComCreator2< CComCreator< CComObject< x > >• \ CComCreator< CComAggOb)ect< x > > > \ -CreatorClass; DECLAREjCLASSFACTORY_EX. You can specify a custom class factory class for ATL to use when creating instances of your object class. To override the default specification o f CComCl a s s F a c to ry , add the DECLARE_CLASSFACTORY_EX macro to your object class and, as the macro parameter, specify the name of your custom class factory class. This class should derive from CComCl ass Facto ry and override its C reatelnstance method. For example: c la s s CMyClass : •••• p u b lic CComCoClass< . . . > { p u b lic : DECLARE_CLASSFAaORY_EX(CMyClassFactory) class CMyClassFactory : public CComClassFactory { • • • STDMETHOD(Createlnstance)(LPUNKNOWN pUnkOuter, REFIID riid , void** ppvObj); ATL also provides three other macros that declare a class factory: _ DECLARE_CLASSFACT0RY2 uses CComCl ass F a c to ry 2 to control creation through a license. # d e fin e 0ECLARE«CLASSFACT0RY2 (1 i c) \ OECLARE.CLASSFACTORY^EX(CComClassFactory2) COM SERVERS ■ DECLARE_CLASSFACTORY_SINGLETON uses C C om C lassFactorySi n g le to n to construct a single CComObjectGlobal object and return the object in re­ sponse to all instaritial ion requests. #define OECLARE.CLASSFACTORY.SINCLETON(obj) \ DECLARE_CLASSFACT0RY_EX(CComC1assFactorySingleton) ■ DECLARE_CLASSFACTORY_AUTO_THREAD uses C C om C lassFactoryAuto- Th read to create new instances in a round-robin manner in multiple apartments. 秦d e fin e DECLARE^CLASSFACTORY^AUTO_THREAD( ) \ OECLARE_CLASSFACTORY_EX(CComClassFactoryAutoTbread) DECLARESLASSFACTORY2 and CComClassFactory2. The DECLARE一 CLASSFACT0RY2 macro defines CComClassFactory2 as your object’s class fac­ tory implementation. CComCl ass Factory2 implements the IC1assFactory2 interface, which controls object instantiation using a license. A CComClassFac- tory2 class object running on a licensed system c .ji provide a runtime license key that a client can save. Later, when the client runs on a nonlicensed system, it can only use the class object by providing the previously saved license key. template class CComClassFactory2 : public IClassFactory2, public CComObjectRootEx, public license { p u b lic : STDMETHOOCCreatelnstance)(LPUNKNOWN pUnkOuter, REFIID riid , void** ppvObj) { • • • i f C lIs L ic e n s e V a lid O ) re tu rn CLASS_E_NOTLICENSEO; • • • return m_pfnCreateInstance(pUnkOuter, riid , ppvObj); } STDM£THOO(CreateInstanceLic)(IUnknown* pUnkOuter, IUnknown* pUnkReserved, REFIID riid , BSTR bstrKey, void** ppvObject); 194 ATL INTERNALS THOD(RequestLicKey)(OWORO dwReserved, BSTR* pbstrKey); THOD(CetLiclnfo)(LICINFO* pLiclnfo); Note that the main difference between CComCl assFactory and CComCl assFac- to ry2 is that the latter class’s C reatelnstance method only creates the instance on a licensed system, that is, when Is L i censeVal i d returns TRUE. The additional C reatelnstanceLi c method w ill always create an instance on a licensed system but only create an instance on an unlicensed system when the caller provides the correct license key. The template parameter to CComClassFactory2 is a class that implements the following static functions: Ve r i fyL i censeKey Returns TRUE if the argument is a valid license key. Get Li censeKey Returns a license key as a BSTR. IsLi censeVal i d Returns TRUE if the current system is licensed. The following is an example of a simple license class: const OLECHAR r lk [ ] ■ OLESTRC^Some runtime license k e y " ) ; class OlyLicense protected s ta tic BOOL Veri fy L i censeKey(BSTR b s tr) { return wrcscmp (bstr, rlk) — 0; static BOOL CetLicenseKeyCdwReserved, BSTR* pBstr) { *pBstr * SysAllocStringC return TRUE; static BOOL IsLi censeVal idQ { return TRUE; } COM SERVERS 195 You specify this license class as the parameter to the DECLARE_CLASSFACT0RY2 macro in your object class. It overrides the _C1 assFactoryCreatorCl ass type­ def inherited fro m CComCoClass. cla ss ATL_NO_VTABLE C E a rP o litic : public CComObjectRootEx, public CComCoClass, DECLARE_CLASSFACT0RY2 (CMyLicense); DECLAREJJLASSFACTORYJyINGLETON and CComClassFaciorySingleton. The DECLARE_CLASSFACTORY_SINGLETON macro defines CComCl a s s F a c to ry - Si ngl eton as your object’s class factory implementation. This class factory only creates a single instance of your class. All instantiation requests return the re­ quested interface • * r on this one (singleton) instance. The template eter specifies the class of the singleton. The class factory creates this singleton object as a member variable, rruObj, of the class factory. template class CComClassFactorySIngleton : public CComClassFactory { p u b lic : void FinalReleaseO { CoOisconnectObject(»..Obj.GetUnknownC), 0); > // IClassFactory STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, p u b li c REFIID riid , void** ppvObj) hRes » nL_0bj.Querylnterface(riid, ppvObj) return hRes; } CComObjectClobal »n.0bj; 196 ATL INTERNALS The following is an example of a simple singleton class: class CMyClass : public CComCoClass< ... > { p u b lic : 0ECLARE_CLASSFACT0RY_SINCLET0N(CMySing1etonClass) You should avoid using singletons if possible. A singleton in a DLL is only unique per process. A singleton in an out-of-process server is only unique per sys­ tem at best, and often not even then due to security and multiuse settings. Typically, most uses of a singleton would be better modeled as multiple instances sharing state, rather than as one shared instance. Also, you should realize that singletons do not w ork under MTS. Most surprising to many people is that an ATL-based inproc singleton that runs in a single-thread apartment (STA) must not require thread affinity, must be thread safe, and must be apartment neutral! This is because the singleton class ob­ ject w ill hand out a direct pointer to every STA-based client. Only a multithreaded- apartment-based client receives the (expected) proxy pointer in this case. DECLAREJCLASSFACTORY_AUTO_THREAD and CComClassFactoryAuto- Tkread. The DECLARE_CLASSFACTORY_AUTO_THREAD macro defines CCom- ClassFactoryAuto 丁hread as your object’s class factory implementation. This class factory creates each instance in one of a number of 叩 artments. You can only lass in an out-of-process server. Basically, this class factory passes every itiation request to your server’s global 一 Modul e instance (discussed later in this chapter), which does all the real work: use thi instant class CComClassFactoryAutoThread : « » ■ STDMETHOOIMP Createlnstance(LPUNKNOWN pUnkOuter, REFIID riid , void** ppvObj) { hRes * Jlodule.Createlnstance(m„pfnCreatelnstance, riid , ppvObj); COM SERVERS The servers 一 Modul e global instance must derive from CComAutoThreadModul e, not simply CExeModule. CComAutoThreadModule derives from CComModule to implement a pool of single-thread apartments in an out-of-process server. By de­ fault, the server creates four STAs per processor on the system. The class factory forwards each instantiation request, in a round-robin basis, to one of the STAs. This allocates the class instances in multiple apartments, which can, in certain situa­ tions, provide greater concurrent execution without the complexity of writing a thread-safe object. The following is an example of a class that uses this class factory: class CMyClass : … ,public CComCoCl as s< … > { p u b lic : DECLARE_CLASSFACTORY^AUTO_THREAD() You’ve seen the object map, which is a fundamental data structure in an ATL server. You’ve seen the various requirements and options for classes, both create­ able and noncreateable, to be listed in the object map. Now, let’s look at the part of ATL that actually uses the object map: the CComModule class, its derived classes, and the global variable of that type called _Modul e. The CComModule Class All COM servers need to support registration, class object management, and life­ time management. Each type of server provides these functions slightly differently, but the basic required functionality is the same for all types of servers. ATL defines the CComModul e class, which provides numerous helper methods supporting server registration, class object management, and lifetime management. Many of these methods simply iterate over the entries in the object map and ask each class in the map to perform the real work. When you create an ATL COM server project using the Visual C ++ ATL project wizard, it generates relatively boilerplate code that uses CComModul e functionality to implement the server. An ATL server contains one global instance of the CCom- Modul e (or derived) class, which you must name _Modul e. The 一 Modul e instance also holds state global to the entire server. 198 ATL INTERNALS The .Module Global Variable An ATL inproc server uses the CComModul e class directly by declaring a global in­ stance of the class in the server’s StdAfx • cpp file. CConModule ^Module; A local server defines a specialized version of CComModule, called CExe- Modul e, in the servers StdAfx • h file and creates an instance of the derived class in the server’s p ro je c t • cpp file. // StdAfx.h class CExeHodule : public CComModule { ft Code d e le te d f o r c la r it y // .cpp CExeModule ^Module; A service-based server also defines a specialized version of CComModul e, called CServiceModule, in the servers StdAfx. h file and creates an instance of the derived class in the server’s p ro je c t. cpp file. // StdAfx.h class CServiceModule : public CConHodule // Code deleted for clarity // .cp CServiceModulc le; A server containing any classes using the auto-threading class object (DECLARE _CLASSFACTORY_AUTO_THREAD) needs to define a specialized version o f CCom- AutoThreadModule (which derives from the wizard-generated CExeModule) in the server’s p ro je ct.h file and create an instance of the derived class in the server’s pro je ct.cp p file. It must override the Unlock method to call the base class’s Unlock method and, when the server’s lock count reaches zero, set the bA cti v i ty flag to tru e and signal a shutdown event COM SERVERS // .h class CAutoModule : public CComAutoThreadModule p u b lic : LONG U n lo c k ( ) { LONG 1 = CComAutoThreadModule: : U niock(); i f (1 == 0) { bActivi ty = true; I I te ll monitor that we transitioned to zero SetEvent ChEventShutdown); } re tu rn 1; / / • 一 P CA d u le ; CComAutoThreadModule itself derives from CComModule to implement an out-of-process server that contains a pool of single-threaded apartments. CCom­ AutoThreadModul e uses CComApartment to manage an apartment for each thread in the module. The template parameter defaults to CComSi mpl eThread­ AI 1 ocator, which manages thread selection for CComAutoThreadModul e. class CComSimpleThreadAIlocator { p u b lic : CComSi mpl eTh r eadAl 1 oc a to r () { «L_nThread - 0 ; } in t C€tThread(CComApartment* /*pA pt*/t in t nThreads) { 1f (4-fflunThread nThreads) mThread 藤 0: return oLjiThread; } in t RLJiThread; CComSi mpl eThreadAI 1 ocator provides one method, CetThread, which selects the thread on which CComAutoThreadModul e w ill create the next object instance. ATL INTERNALS You can write your own apartment-selection algorithm by creating a thread alloca­ tor class and specifying it as the parameter to CComAutoTh readModule. For example,the following code selects the thread (apartment) for the next ob­ ject instantiation randomly (though it has the downside of using a C runtime library function to get the random number). class CRandomThreadAllocator { p u b lic : in t GetThread(CComApartment* /*pApt*/, int nThreads) { return rand () % n^hreads; Thi'n you need to derive your _Module instance class from CComAutoTh read- Modul e and specify your new thread selection class as the template parameter. cla ss CAutoModule : p u b lic CComAutoThreadModu"lenstrr '<*''rs itv sianre handle isn’t available at the time the CRT runs constructors of global objects. COM SERVERS HRESULT InitC_ATL>OB3MAP_ENTRY* p, HINSTANCE h, const CUIO* p i ib id * NULL); vo id 丁e rm (); An inproc server initializes and uninitializes its —Modul e object in its D11 Mai n function. extern "C“ BOOL WINAPI OilMain(HINSTANCE hlnstance. DWORO dwReason, LPVOID /*lpReservedV) { if (dwReason — DLL-PROCESSJHTACH) { _Module.Init(ObjectMap, hlnstance, ALIBID^ATLINPROCLib); DisableThreadLibraryCal1s(hlnstance); > e lse i f (dwReason 漏 篇 DU 一 PROCESS一 DETACH) .Module.Term(); return TRUE; // ok Both local servers and servicc-based servers initialize and uninitialize their re­ spective 一Modu 1 e objects in their Wi nMai n functions. Here’s an example for a local server. A service calls the Windows NT Service Control Manager from Wi nMai n rather than running a message loop, but otherwise operates the same. extern “C" int WINAPI «tWinMain(MINSTANCE hlnstance, HINSTANCE /*hPrevInstance*/, LPTSTR IpCmdLine, int /♦nShowCmd*/) { • • • _Modu1e.Init(ObjectMap. hlnstance, ALIBIDJMLLOCALLib); • •參 HSC fflsg; while (GetMessage(&msg, 0, 0, 0)) OispatchMessage(&msg); • • • .Module.Term(); » • • } When you call the In i t method on the _Modu1 e instance ATL not only in itia l­ izes the CComModul e instance but also iterates over the object map and calls the 202 ATL INTERNALS Obj ectMai n static member function of each class in the map with the b S ta rti ng parameter set to true. When you call the Term method, ATL releases any refer­ ences to class objects that it has cached in the object map, then calls the Obj e c t- Main function of each class in the map with the b S ta rti ng parameter set to fa ls e . The CComModule Registration Support The CComModul e class has extensive support fo r registration and unregistration of COM objects and servers. The UpdateRegistryClass Method The UpdateRegi s tryC l ass method enters or removes an object’s standard class registration information (ProgID, version-independent ProgID, class description, and threading model) into or from the system registry. It calls on the Regi s te r- C1 assHel per and Unregi sterC I assHel per methods to perform the actual task. // Standard registration entries method HRESULT WINAPI UpdateRegi stryClass(const CLSID& clsid, LPCTSTR lp szProgID, LPCTSTR IpszV crlndP roglO , UINT nOescID, DWORD dwFlags, BOOL bR egister); 1 II Helper methods I 1 HRESULT WINAPI Regi sterCIassHelper(const CLSID& clsid , LPCTSTR IpszProgID , I LPCTSTR IpszVerlndFrogID, UINT nOescID, DMORO dw Flags); HRESULT WINAPI UnregisterCIassHe1per(const CLSI0& clsid . LPCTSTR IpszProglO , LPCTSTR IpszVerlndProglO); The UpdateRegi stryC l ass method and its helper methods typically aren’t used that often because they're limited in the registration entries they support The RegisterServer and UnregisterServer Methods More crften, you'll use the Regi s te rS e rv e r o r U nregi s te rS e rv e r methods. HRESULT Regi sterServer(BOOL bRegTypeLib « FALSE, const CLSID* pCLSID ■ NULL); HRESULT Unregi sterS erver(co n st CLSIO* pCLSID ■ NULL); COM SERVERS 203 HRESULT U nregisterS erve「(BOOL bUnRegTypeLib, const CLSID* pCLSID - NULL); The Regi sterS erver method updates the system registry for a single class object when the pCLSID parameter is non-NULL. When the parameter is NULL, the method calls the UpdateRegi s try method for all classes listed in the object map. When the bRegTypeLib parameter is TRUE, the method also registers the type library. The Unregi sterS erver method removes the registry entries for a single class when the pCLSID parameter is non-NULL. When the parameter is NULL, the method calls the UpdateRegi s try method for all classes listed in the object map. The over­ loaded method accepting a bUnRegTypeLi b parameter w ill also unregister the type library when the parameter is TRUE. Inproc servers call on this support from their Dl 1 Regi sterServer and Dl 1 Unregi ste rSe rve r functions: STDAPI D l1Regi sterServer(voi d) { return M odule. Regi sterServer (TRUE); > STDAPI Dl 1 Un re g i s t e rSe rve r (v o i d) { return .Module.Unregi sterServer(TRUE); 麵 議 :,喝 $羅 . A local server’s Wi nMai n code registers or unregisters the server’s resource script entries and then calls the _Modul e object’s Regi sterS erver method to do the same for each class in the object map. i f (1 strcmpi(1 pszToken, -T(,,UnregServerH))*-0 ) { M odule.UpdateRegi stryFromResource(IDfLATLlocal, FALSE); nR«t 键 Jtodule.UnregisterServer(TRUE); • « • break; > 1f (1 strcmpi (IpszToken f JT("Re9S€rver••))•*<)) { .Module.UpdateRegistryFromResourceCIDRJVTLLocal, TRUE): nRet • _Hodule.RegisterServer(TRUE); ATL INTERNALS The UpdateRegistryFromResource Methods Notice that a local server first calls the UpdateRegi stryFromResource member function to register and unregister the server-specific entries before registering the class-specific entries. The UpdateRegi stryFromResource method is the same method that your class uses when you specify the DECLARE—REGISTRY_RESOURCE o r DECLARE一REGISTRY—RESOURCEID macros, discussed previously in this chapter. Note that when you define the preprocessor symbol _ATL_STATIC__RECISTRY, the UpdateRegi stryFromResource method maps to UpdateRegi stryFromRe- sourceS, which is a function that is statically linked into your server. When you do not define this preprocessor symbol, the method m^ps to UpdateRegi stryFrom - ResourceD, which is a function in a tl .d ll to which your server dynamically links. When you don’t define the preprocessor symbol, you must distribute a tl • d l 1 along with your server. # ifd e f _ATL^STATIC_RECISTRY #define UpdateRegi stryFromResource UpdateRegi stryFromResourceS #else #define UpdateRegi stryFromResource UpdateRegi stryFromResourceD # e n d if // Resource-based registration HRESULT WINAPI UpdateRegistryFromResourceOCLPCTSTR IpszRes, BOOL b R e g is te r, s tr u c t ^ATL^RECMAP.ENTRY* pMapEntries » NULL); HRESULT WINAPI UpdateRegistryFromResourceD(UINT nResIDf BOOL b R e g is te r, s tr u c t JVTL一 REGMAP一 ENTRY* pMapEntries « NULL); # ifd e f JVT匕 STATIC一REGISTRY II Statically linking to Registry COM Ponent HRESULT WINAPI UpdateRegistryFromResourceS(LPCTSTR IpszRes, BOOL b R e g is te r, s tr u c t _ATL_REGMAP_ENTRY* pM apEntries * NULL); HRESULT WINAPI UpdateRegistryFromResourceS(UINT nResID, BOOL b R e g is te r, s tr u c t «ATIRECMAP.ENTRY* pMapEntries ■ NULL); 参e n d if The Type Library Registration Methods There are a few other registration helper methods provided by CComModul e. // Registry support (helpers) HRESULT RegisterTyp^LibO ; HRESULT RegisterTyp^Lib(LPCTSTR lpszlndex); COM SERVERS 205 HRESULT UnRegisterTypeLibC); HRESULT UnRegi sterTypeLib(LPCTSTR lp s z ln d e x ); Regi sterTypeLi b, as you iriight expect, registers the type library contained in your server’s resources. UnRegi sterTypeLi b unregisters it, of course. These two methods expect the type library to be present in your server’s resources as a cus­ tom resource of type TYPELIB with integer resource identifier 1, by default. You cre­ ate such a resource by adding the following line to your server’s . rc file: 1 TYPELIB "A T L In te rn a ls .tlb " You can embed multiple type libraries in a server, though it’s an unusual thing to do. You simply need to give them unique integer resource identifiers. 1 TYPELIB "A T L In te rn a ls .tlb " 2 TYPELIB MAT L In te rn a ls E x .tlb M To register or unregister the second type library, you must call the Regi sterT ype­ Li b or UnRegi sterTypeLi b method, respectively, and specify a string in the form "\\N ", where N is the integer index of the TYPELIB resource. The following lines register both type libraries from the previous resource script example: —Module.RegisterTypeLib (); —Module.RegisterTypeLib (_T ("\\2 M)) The RegisterProgID Method The last registration helper method is Regi s te r ProgID. It does what you might expect. It creates the specified ProgID entry under HKEY_CLASSES_R00T w ith the specified description. Additionally, it creates the CLSID named value for the ProgID, which points at the specified CLSID. It requires the string version of the CLSID. s ta tic HRESULT Reg sta tic HRESULT Createlnstance(IUnknown* punkOuter, Q** pp) 208 ATL INTERNALS return T::_CreatorClass::CreateInstance(punkOuter, 一 u u id o f(Q ), (void**) pp); > template sta tic HRESULT CreateInstance(Q** pp) { .4 return T: :_CreatorClass::CreateInstance(NULL, 一 u u id o f(Q ), (void^*) pp); You use these two methods like this: ISpeaker* pSpeaker; / / Creates nonaggregated instance HRESULT hr = CDemagogue::Createlnstance C&pSpeaker); II Creates aggregated instance (assuming the class supports // aggregation) HRESULT hr =* COemagogue::Createlnstance (punkOuter, &pSpeaker); Note that the use of the _ uuidof operator in the template functions means you do not have to specify the interface ID for the interface that you want on the newly instantiated object. The compiler gets the ID from the type of the interface pointer variable you pass as an argument to the C reatelnstance method. Server Optimization Compilation Options ATL supports building a server optimized for minimum size or minimum de­ pendencies. There are three preprocessor symbols you define to affect the server optimization. _ATL_MIN_CRT The server doesn’t link with the standard C/C + + runtime library. -ATL^DLL The server dynamically links to the a tl. dl 1 utility function library. -A T IS T AT IC 一 R EC I STRY The server statically links to the component regis­ trar support. COM SERVERS 209 When you define the _ATIMIN_CRT preprocessor symbol, your server does not link with the C/C++ runtime library, and ATL provides an implementation of the ma飞飞oc, realloc, free, new, and delete functions. You should not call any other C/C+ + runtime library functions when you define this symbol.9 If you do, you will receive the following (unintuitive) error message: LIBC.libCcrtO.obj) : error LNK2001: unresolved external symbol 一 main This error means you’ve called one or more functions in the C runiime library im- plemenlation in the CRT but, because you defined the _ATL一MIN一CRT preproces­ sor symbol, you didn’t link with the library. Either eliminate the call(s) to the runtime library functions or remove the preprocessor symbol definition. Wizard­ generated ATL projects define _ATI_一MItv一CRT for all release builds and do not define the symbol for all debug builds. Note that a server also requires the CRT when it uses exception handling or needs to run the constructors and destructors of objects declared as global or static variables. ATL provides a few utility functions commonly used by servers plus the Com­ ponent Registrar object (used to parse registry scripts) in a tl .d ll. When you define the _ATIDLL preprocessor symbol, your server dynamically links to the utility functions in a tl .d l 1, which means your server now requires a tl .d l 1 to be present. The absence of this symbol causes your server to link to the utility func­ tions statically, which doesn’t require a tl .d l 1 but increases the size of your server. Note also that ATL provides two versions of a tl .d l 1 — a Unicode version that only works on Windows NT and an ANSI version for Windows 9x. When you define the _ATL_STATIC_RECI STRY preprocessor symbol, your server statically links with the component registrar, which increases the size of your server when the server uses registry scripts (the default). The absence of this sym­ bol causes your server to link dynamically to the component registrar in a tl .d ll. A wizard-generated project generates various build configurations using these symbols. The Debug build configurations do not define any of these three sym­ bols. The RelMi nSi ze build configurations define _ATL一MIN一 CRT and __ATL一DLL. The Rel Mi nDependency build configurations define _ATL_MIN_CRT and _ATI STATICLREGISTRY. 9This isn't strictly true. You can request that the Microsoft C/C+ + compiler generate inline code for some runtime library functions by specifying the #pragma i ntri nsi c compiler directive. For example, you can generate inline code for the memcmp and strcmp functions in this manner. Other “runtime li­ brary functions," such as a llo c a and set jmp, always generate inline code regardless of the setting of the intrinsic pragma. References to such functions in this case do not require the C/C++ runtime library. 210 ATL INTERNALS Summary The object map is the primary data table used by the CComModul e-derived _Mod- u le object to register and unregister the classes in the server. CComModule pro­ vides numerous helper methods you can use to register and unregister your server, the classes in the server, and the component categories in the classes. An ATL server creates a class factory for a class, as needed, using the class cre­ ator fimction entry in the object map. This class factory then creates instances of the class using the instance creator function entry in the object map. Typically, you inherit a default specification for the implementation of these two creator classes when you derive your class from CComCoClass. However, with the appropriate macro entries in your class, you can override these defaults and provide a custom class factory and/or custom instance creator function. CComCoClass has some useful utility functions for reporting rich errors to a client using the COM E rro r In fo protocol. The class also has convenient instanti­ ation methods that create an instance and query for the proper interface based on the type of the provided interface pointer variable. Additionally, ATL provides three preprocessor symbols that allow you to build a server tailored for the smallest size or external dependencies. CHAPTER 5 Interface Maps W hile Chapter 3 discussed how ATL implements IUnknown, only AddRef and Release were covered completely. This chapter takes a look first at the require­ ments that COM makes on an object’s implementation of Q uerylnterface and second on how ATL supports those requirements while still providing flexibility and extensibility. Recall: COM Identity From a client perspective, the rules of AddRef and Release are fairly stringent. Un­ less the client is careful about their use, objects could go away before expected or stay around too long. The object, however, is allowed to implement AddRef and Rel ease in any number of ways, depending on how it would like to manage its own lifetime, for example, as a heap based, stack based, or cached object. On the other hand, Q uerylnterface is pretty easy to get right on the client side. Any client can ask an object if it supports any other functionality with a simple call. However,clients expect certain relationships between the interfaces on a COM object. These expectations form the laws of COM identity. The Laws of COM Identity The laws of COM identity say the following things about how an object must expose its interfaces via Q uerylnterface. ■ A client must be able to get directly to any interface implemented by the object via Que ry In te rfa c e . • An object’s interfaces must be static for its life. ■ Querylnterface for IUnknown must always succeed and must always return the same pointer value. Direct Access to All Interfaces The COM specification states that an implementation of Que ry In te rfa ce must be “reflexive, symmetric and transitive.” What this means is that, given an interface, a client must be able to use it to get directly to any interface implemented on the ATL INTERNALS object, including the interface the client is using to perform the query. These rela­ tionships are mandated to maintain an object's identity in the face of multiple ref­ erences to the same object. If these relationships are not upheld, a client could find itself with some code that doesn’t work just because it has asked for the interfaces in the wrong order. Given a properly implemented Q uerylnterface, query order will not matter. Static Types Each object may decide for itself whether it wants to expose an interface via Q uerylnterface, regardless of the class to which it belongs. However, once it has been asked and has answered either “Yes, I support that interface” or “No,I don’t support that interface," it must stick to that answer. The reason for this is simple: Once an object answers the query, it may never be asked again. For example, a client may pass a resultant interface pointer to another client, who never has to ask the object at all. The potential for clients “talking amongst themselves" means that an object cannot use Q uerylnterface to make client-specific decisions, for example, deci­ sions based on security constraints. The object also may not use Q uerylnterface to make context decisions that may change during the life of an object, for example, the time of day. If a client caches an interface pointer returned when the context is favorable, it may not ask again when the context has changed. An Object’s Apartment-Specific Identifier The remoting layer of COM uses the four-byte pointer returned when querying for IUnknown as an object’s unique identifier in that apartment. Clients may also com­ pare IUnknown*s as an identity test. For example: bool AreEqualObjects(IUnknown* punkl, IUnknown* punk2) { if ( !punkl || !punk2 ) return false; IUnknown* punka = 0; punkl->QueryInterface(IID_IUnknown, (void**)&punka); IUnknown* punkb = 0; punk2->QueryInterface(IID_IUnknown, (void**)&punkb); bool b = (punka == punkb); punka->Release(); punkb->Release(); re tu rn b; } In fact, the ATL smart pointer classes have a method called IsEqualO bject for performing just this comparison: INTERFACE MAPS 213 STDMETHODIMP CBal1 ::SetPlaySurface(IRollSurfaceA prsNew) { if ( i»_sprs. IsEqual Object (prsNew) ) return S_OK; However, while COM dictates that the pointer value of IUnknown must always be the same, it places no such restrictions on any other interface. This particular loophole leads to such techniques as tear-off interfaces, discussed later in this chapter. Nothing Else As long as the three laws of COM identity are upheld, an implementation of Query­ ln te rfa c e can be developed using scenes from your most vivid fever dreams. Frankly, I doubt you’ll be able to come up with any techniques wackier than those already known, which I’ll present during the rest of this chapter. However, if you do, ATL’s implementation of Q uerylnterface is fully extensible, as you’ll see. Table-Driven Querylnterface The Raw Interface Map Remember from Chapter 3 that ATL’s implementation of Q uerylnterface is called In te rn a l Que ry ln te rfa c e and is provided as a static member function of CComObj ectRootBase (shown here with debugging extensions removed): S ta tic HRESULT WINAPI CCoa0bjectRoot8ase::InternalQueryInterface( void* pThis, const ^TUINTMAP.ENTRY* pEntries, REFIIO i1d, void** ppvObject) { // First entry in the com map should be a simple map entry ATLASSERT(pEntri€S->pFunc — ^TUSIHPLEMAPENTRY); HRESULT hRes • AtlInternalQuerylnterfaceCpThis, pEntries, iid , ppvObject); return hRes; } ATL INTERNALS I’ll show you the implementation of internal function, A tl InternalQ uery- In te rfa c e , later. First, lefs discuss the ATL—INTMAP_ENTRY structure, an array that is passed to InternalQ uerylnterface : s tr u c t JVTL-INTMAP—ENTRY { • const IID* piid; DWORD dw; •. . . •: ;• J 、:. •• . JVTL.CREATORARCFUNC* pFunc; , > ; ■ Each entry provides a pointer to an interface identifier (IID), a pointer to a function to retrieve the requested interface, and a user-defined parameter to pass to the func­ tion. Functions that fit into this table must have the following signature: HRESULT WINAPI InterfaceMapFunction( void* pvThis, // Object's this pointer REFIID riid, // Interface requested LPVOID* ppv, / / Storage fo r requested in te rfa c e DWORD dw); // dw from the interface map entry The job of the interface m叩 function is to take the object’s th i s pointer, the interface being requested by the client, and the dw argument and to return the ap­ propriate interface pointer in the ppv argument The function is free to do whatever it likes, within the laws of COM identity, to perform this magic. For example, the following function assumes that the dw member is the offset of the vptr from the th i s pointer, that is, that we’re using multiple inheritance (MI) to implement the interface: HRESULT WINAPI _ M I(v o id * pvThis, REFIID r i i d , LPVOID* ppv, OWORO dw) { *ppv * (BYTE*)pvThis + dw; rei nterpret_cast(*ppv)->AddRefC); re tu rn S_OK; } To fill in the _ATI__INTMAP_ENTRY for use with this function, we need to be able to calculate the offset of a vptr from the base, preferably at compile time. To help with this chore, ATL provides an interesting macro: #define JML.PACKINC 8 #d€fine o ff se to fc lass(base, derived) \ ((DWORD)(statiC-Cast((derived*)_ATL 一 f>ACKINC))-_ATLJ>ACKINC) INTERFACE MAPS 215 The o ffse to fcla ss macro makes it look as if we’re asking the compiler to de­ reference a pointer with the value 8, which is not such a great value for a pointer.1 Instead, what we’re doing is asking the compiler to imagine a pointer to an object and to calculate the difference between that and a member inside that object, that is, a vptr associated with a specific base class. The o ffs e to fc la s s macro per­ forms the same offset calculation the compiler does whenever it needs to perform a s t a t i c 一 cast to a base class. Using o ffs e to fc l ass allows us to fill the entry in an interface map like so: class CBeachBall : publi c CComObjectRootEx, public ISphere, public IRollableObject, public IP1aything { p u b lic : const s ta tic JVTU_INTMAP_ENTRY* WINAPI _CetEntries() { s ta tic const - ATLWINTMAP-.ENTRY _ e n trie s [] « { { &IID一IUnknown, offsetofclass(ISphere, CBeachBall), JVTL.SIMPLEMAPENTRY } , { &IID_ISphcre, offsetofclass(ISphere, CBeachBall), _MI >, { AIID_IRol1ableObj ect, offsetofclass(IRol1ableObj ect, CBeachBall), _MI >f { iiIID_IPlaything, offs«tofclass(IPlaythlng, CBeachBall), _MI >, {NULL. 0, 0} return ^entries; HRESULT ^InternalQucryInterface(REFIID 1id, void*A ppvObject) { return InternalQuerylnterface(thisp .CetEntriesQ, iid , ppvObject); } Besides the population of the interface map, there are a couple of interesting things to notice about this code snippet. First, the .InternalQ uerylnterface function calls _G etE ntri es to retrieve the static interface map and forwards it to the Internal Q uerylnterface static member function in CComObj ectRoot­ Base. The ^.Internal Q uerylnterface function is required by CComObj ect et al. to implement Q uerylnterface. 1 Microsoft didn't invent this particular oddity. The standard C runtime library comes with a macro very much like o ffs e to fc l ass called o ffs c to f. 216 ATL INTERNALS Second, notice that IUnknown is the initial entry in the list and uses __ATL一 SIMPLEMAPENTRY instead of _MI. As I discussed in Chapter 3, the first entry is the one used for IUnknown and is required to be a simple entry. A simple entry is a special case that indicates that the interface is being exposed using multiple inheri­ tance and that an offset is all that is needed to calculate the requested interface pointer. _ATL 一 SIMPLEMAPENTRY is a special value used in the pFunc field of the _ATL__INTMAP_ENTRY structure to indicate this case: # d e fin e JM LWSIMPLEMAPENTRY ( (^ATL.CREATORARGFUNC*)1) When A tllnternalQ uerylnterface encounters this special value, it knows how to perform the offset calculation just like the example function, 一 MI. Since _ATL 一 SIMPLEMAPENTRY completely replaces the need for a function that performs the offset calculation, ATL provides no interface map functions like _MI, although it provides others, as I’ll discuss later. Convenience Macros You may ei\joy writing the required 一 In te r n a l Que r y ln te r fa c e and _ G e t- E n tri es methods, as well as the CetUnknown method discussed in Chapter 3,but I do not To write these functions and to begin the static definition of the interface m冲 , ATL provides BEGIN一 COM一 MAP. # d e fin e BECIN^COMJUPCx) p u b lic : \ typ e d e f x _ComMapClass; \ • • • IUnknown* _CetRawUnknown() { \ ATLASSERTCCetEntriesO[0 ] .pFunc _^TL.SIMPLEMAPENTRY); \ return (IUnknown*)((int)this-f„CetEntrics()->dw) ; \ IUnknown* CetUnknown() { return _CetRawlinkno«vn(); > \ \ * HRESULT _Interna1QtjeryInterface(R£FII0 iid , void** ppvObject) \ { return InternalQueryInterface(this, „CetEntries(), iid , \ ppvObject); } \ \ const s t a t ic JVTL.INTMAP.ENTRY* WINAPI _ C e tE n trie s () { \ s t a t ic const JVTL-INTMAP—ENTRY . e n t r ie s f ] ■ { To zero-terminate the interface map and to round out the _C etE ntri es iraple- mentation, ATL provides END_C0M_MAP. INTERFACE MAPS 217 #define ENO.COM.HAPC) { NULL, 0. 0 }}; return —entries; } \ v ir t u a l ULONC STDMETHODCALLTYPE AddRef( v o id ) * 0 ; \ v ir t u a l ULONC STDMETHODCALLTYPE Release( void) - 0; \ STDMETHOO(Querylnterface)(REFIID, void**) - 0; One other interesting thing that END—COM一MAP does is to provide another set of pure virtual member function definitions for Q uerylnterface, AddRef, and Rel ease. This makes calls to IUnknown member functions unambiguous while calling them in the member functions of your ATL-based classes. To populate each entry in the interface map, ATL provides a set of macros be­ ginning w ith the C0M_INTERFACE一 ENTRY prefix, the simplest and most useful be­ ing COM_INTERFACE_ENTRY itself: ♦define CWUINTERFACE..ENTRY(x)\ {^l^T L _ IID 0 F (x), offsetofclass(x, ^ComMapClass), \ _ATL_SIMPLEMAPENTRY } , Notice the use of _ComMapClass as the name of the class associated with the static interface map. This type is provided by BEGIN_C0M_MAP. The _ATL_IID0F macro, on the other hand, is ATL’s way of turning an interface type name into the corresponding globally unique identifier (GUID). Based on the presence or absence of the _ATL_N0_UUID0F symbol, ATL w ill either use the VC+ + -specific 一 u u id o f operator2 or it w ill use the C preprocessor’s token pasting operator #ifndef JUl^NOJiUIOOF #define _ATU.II00F(x) —uuidof(x) fe ls e fdefine J^TL-IIOOFCx) IID_##x #endif Using these macros, the interface map can be defined much more simply than the previous example: class CBeachBal1 : public CComObjectRootEx, public ISphere, public IRollableObject, public IPlaything { 42The _ declspec(uuidO ) and _ uuidof operators are discussed in Chapter 3. 218 ATL INTERNALS p u b lic : BEGIN_COM_MAP CCBeachBal 1) COH.INTERFACE_ENTRY(ISphere) COM JLNTERFACE^ENTRY(IRol1a b le O b je ct) COM_INTERFACE_ENTRY(IPlaything) END一 COfl_MAPC) AttlntemalQuerylnterface Checking for IUnknown In te rn a l Q uerylnterface delegates to a global function, A tl In te rnal Que ry- In te rfa c e , to provide its implementation. Before walking the table of interface en­ tries, A tl In te rnal Q uerylnterface checks the IID of the request. If IUnknown is requested, the first entry is pulled from the table and handed back immediately, without walking the rest of the table. This is a welcome optimization, but it’s more than that. It’s absolutely necessary that IUnknown not be calculated by calling a function. Functions can fail and functions can return different values for the same interface identifier, both of which actions violate the laws of COM identity. The practical meaning for your classes is that the first entry must always be a simple one. Luckily, ATL is laden with assertions to this effect, so as long as you test your implementations at least once in debug mode before shipping them to your cus­ tomers, you should be safe (on this note, anyway). Walking the Table At each entry in the table, a decision is made based on whether the p i i d member, a pointer to the interface identifier fo r that entry, is NULL or not. When it is not NULL, then the IID of the entry is compared with the IID of the request. When a match is found, the function referenced by pFunc is called and the result is returned to the client. When there is no match, the search advances to the next entry in the table. On the other hand, when the p i i d member is NULL, the pFunc is called, no mat­ ter what the IID of the request is. If the result is S—OK, the result is returned to the client. Otherwise, the search continues with the next entry. This behavior is used fo r any o f the COM_INTERFACE_ENTRY_XXX_BLIND macros, for example, COM_ INTERFACE_ENTRY」 \GGREGATE—BLIND. Implementation The following is the implementation of A tl In te rn a l Q uerylnterface. INTERFACE MAPS 219 ATLINLINE ATLAPI A tlIn te rn a lQ u e ry ln te rfa c e C v o id * const J^TL.INTMAP.ENTRY* REFIID v o id ** p T h is, p E n trie s , i i d , ppvO bject) ATLASSERT(pThis !_ NULL); ATUSSERTCpEntri«s->pFunc *» JO■匕 SIMPLEMAPENTRY); i f (ppvO bject — NULL) re tu rn E^POINTER; ♦ppvObject - NULL; i f CInlinelsEqualUnknoivnCild)) { IUnknown* pUnk ■ (IUnknown*)((int)pThis^pEntries->dw); pUnk•>AddRefO; ♦ppvObject - pUnk; re tu rn S.OK; } while CpEntr1es->pFunc !» NULL) { BOOL bBlind • (pEntries->piid 撕 NULL); if CbBlind U imineIsEqualCUIDC*(p£ntri€S->piid), lid )) { if (p£ntries->pFunc 抑 ^ATL.SIMPtEMAPENTRY) { ATLASSERT(!bBlind); IUnknown^ pUnk ■ (IUnknown*) ((in t)p T h i s<»-pEntries->dw); pUnk->AddRef(); ♦ppvObject • pUnk; re tu rn S.OK; > e lse { HRESULT hRes « pEntr1es->pFunc(pThis, iid , ppvObject, pEntri€s->dw); if (HRes — S.OK 丨丨(IbBlind Mr FAILEO(hRes))) return hRes; pEntries-hf E从FRE町IoNiEnruter X J 220 ATL INTERNALS Multiple Inheritance To support multiple inheritance of interfaces, ATL provides four separate interface entry macros. Two are for straight casts and two are for branching casts. A straight cast is a s ta ti c 一 cast that the compiler needs no extra information to perform; that is, there are no ambiguities. On the other hand, a branching cast is used when a class has several base classes that all derive from the same base class themselves. Since a straight cast to the common base class would be ambiguous, the compiler needs to be provided with the inheritance branch to follow to resolve the ambiguity. Straight Casting COMJNTERFACE_ENTRY and COMJNTERFACE 一 ENTRYJID As I mentioned, COM_INTERFACE_ENTRY is the macro you’re going to use most of the time. Its close cousin is COM_INTERFACE.ENTRY_IID: 参d e fin e C0M.INTERFACE.ENTRY.II0C1 Id , x) \ { 4 iid , o f f setof cl ass (x, .ComMapClass), J\TUSIMPLEMAPENTRY}, This latter macro allows you to specify the IID separately from the name of the interface. The classic usage of this macro is to avoid ambiguity. Imagine you have two interfaces that derive from the same base interface. For example, interface IGlobe : ISphere {}; interface IPIanet : ISphere {}; If you have a class that derives from both of these interfaces, the compiler won’t know which base class to cast to if you use COH_INTERFACE_ENTRY for ISphere : class CDesktopClobe : public CComObjectRootEx. public IGlobe, p u b lic IP Ia n e t { p u b lic : » • • BECIN_COM__MAP(CDesktopClobe) COM-JNTERFACE_ENTRY(ISphere) / / ambiguous C0M_INTERFACE.ENTRY(IC1obe) C0M_JNTERFACE_ENTRY(IP1anet) END_C0MJ1APC) / / ISphere methods INTERFACE MAPS Figure 5.1. CDesktopGlobe inheritance hierarchy / / IGlobe methods // IPlanet methods The problem is more easily seen by looking at the inheritance hierarchy, as shown in Figure 5.1, The figure shows that we have two interfaces that CDesktop­ Globe has inherited from more than once, IUnknown and ISphere. IUnknown is not a problem because ATL handles it specially by choosing the first entry in the interface map (as I discussed earlier). ISphere is a problem, though, because we have two of them, IC lobe and IP1 anet. Each base interface has a separate vptr that points to a separate vtbl. Even though we have a shared implementation of all the methods of ISphere, and therefore duplicate entries in both the IC lobe and the IP lanet \tbls, the compiler still needs us to pick one. COM_INTERFACE_ ENTRY_IID allows us to resolve this ambiguity: class CDesktopGlobe : public CComObjectRootEx, public IClobe, public IPlanet { p u b lic : ATL INTERNALS BEGIN_COM_MAP(CDesktopClobe) COM_INTERFACE^ENTRY_IID(IID- IS pheret IG1obe) / / unambiguous C0M_INTERFACE_ENTRY(IC1obe) COM_INTERFACE_ENTRY(IPlanet) END 一 COM_MAP() In this case, because we have shared implementations of the ISphe re methods in our implementation of IG1 obe and I PI anet, it doesn't really matter which one we hand out. Sometimes it matters very much. As I mentioned in Chapter 3, COM_INTERFACE_ENTRY_IID is often used when exposing multiple dual inter­ faces, each of which derives from ID i spatch. Using ID i spatchlm pl, we provide base class implementations of ID i spatch that are different based on the dual inter­ face we’re implementing. In fact, any time you have multiple implementations of the same interface in the base classes, you must decide which implementation is the “default.” Imagine another example of a base class interface implementation that has nothing to do w ith scripting: tem plate cla ss ISpherelm pl : p u b lic Base { . . . } ; Using ISpherelmpl would look like this: class CDesktopClobe : public CComObjectRootEx, publ1c ISpherelmpl, public ISpherelmpl { p u b lic : • • • BECIN_COM_MAP(CDesktopClobe) COM_INTERFACE_ENTRY_IIDCIID_ISphere, ICIobe) / / Default ISphere C0M_INTERFACE_ENTRY(IC1obe) COM_INTERFACE_ENTRY(IPlanet) ENO_COM_MAP() Here’s the problem: if the client queries for IG1 obe (or ISphe re) and calls ISphere methods, it w ill get different behavior than if it were to query for I PI anet and call ISphere methods. Now the client has just the kind of order-of-query prob- INTERFACE MAPS 223 lem that the laws of COM identity were built to prohibit. Multiple implementations of the same base interface clearly violate the spirit of the laws of COM identity, if not the letter. Branch Casting COMJNTERFACE 一 ENTRY2 and COMJNTERFACE 一 ENTRY2JID Both C0M—INTERFACE-ENTRY2 and C0M_INTERFACE_ENTRY2.IID are also simple entries meant for use with multiple inheritance: fd e fin e C0M.INTERFACE.ENTRY2(x, x2) \ { UTLJCIOOF(x), \ (DWORD)((x*)(x2*)(C-CoirMapClass*)8))-8, \ JVTUSIMPLEHAPENTRY}, fdefine C0ILIMTERFACE«ENTRY2J[ID(iidf x, x2) \ { A iid , \ (DWORD)((x*)(x2*)(CComMapCla$s*)8))-8# \ JVTU.SIMPLEMAPENTRY}, C0M_INTERFACE_ENTRY2 is very much like COM_INTERFACE_ENTRY_IID because it allows you to resolve the multiple bases problem. For example: class CDesktopGlobe : public CComObjectRootEx, public IClobe, public IPlanet { p u b lic : BECIN_COM_MAP(CDesktopClobe) COMJNTERFACE_ENTRY2(ISphere, IC lobe) / / Use the IClobe branch C0M_INTERFACE_ENTRY(IC1obe) COM_INTERFACE_ENTRYCIPlanet) END_COM一 MAP() This macro performs its magic by allowing you to specify two things: the interface to expose (e.g., ISphere), and the branch of the inheritance hierarchy to follow to get to the implementation of that interface to use (e.g., IC1 obe). This macro is 224 ATL INTERNALS slightly different from COM__INTERFACE_ENTRY_IID in that the interface is speci­ fied by name instead of by IID. If you’d like to be very explicit about both, use C0M_INTERFACE_ENTRY2_IID: class CDesktopClobe : public CComObjectRootEx, public IGlobe, p u b lic IP Ia n e t { p u b lic : • ■ • BEGIN_C0M_MAP(CDesktopG1obe) COM.INTERFACE.ENTRYZ^IIDC&IID.ISphere, ISphere, IGlobe) C0M_INTERFACE_ENTRY(IGlobe) COM_INTERFACE^ENTRY(IPlanet) ENO_COM_MAP() Since C0M_INTERFACE_ENTRY2 [ - I I D ] provides no extra functionality be­ yond that provided by COM—INTERFACE_ENTRY[_IID], I tend to always use the latter and forget the former. Handling Name Conflicts One of the problems with multiple inheritance is that of name collisions. Imagine the following interfaces: interface ICowboy : IUnknown { HRESULT D raw(); interface IA rtist : IUnknown { HRESULT D raw(); Because both Draw methods have the same signature, using straight multiple in­ heritance requires a single shared implementation: II Ace Powell was a cowboy/artist who lived in II to his death in 1978. I'd like to thank Tim II example, which I have used to death for years class CAcePowell : the western US from 1912 Ewald for this fabulous INTERFACE MAPS 225 public CComObjectRootEx, public ICowboy, public IA rtist { publ i c •• B EGIN_COM_MAP(CAc e Powe11) COM—INTERFACE一 ENTRY(ICowboy) COM_INTERFACE_ENTRY(IArtiSt) END_COM_MAP() • • • STDMETHODIMP Draw() { /* Act as a cowboy or an a rtist? */ } }; Since the implied meaning of Draw is very different for an artist than it is for a cow­ boy, we’d like to be able to provide two Draw implementations. For that, we use a technique long known to the C 十 》community that I’ll call “forwarding shims.”3 The problem is that C + 十 has no syntax to be able to distinguish methods with the same signature from different bases in the derived class. For example, the fol­ lowing is not legal C+ + . class CAcePowell : public CComObjectRootEx, public ICowboy, , public IA rtist { p u b lic : B ECIN_COM_MAP( CAc e Powe11) COM_INTERFACE_ENTRY(ICowboy) COM_INTERFACE_ENTRY(IArtist) END—COM 一 MAf>() » • • STDMETHODIMP I A r t is t : : D r a w ( ) ; / / e r ro r STDMETHODIMP ICowboy: : Draw(); / / error However, we can certainly distinguish the methods in individual base classes, for example: s tru c t 一IArtist : public IArtist { STDMETHODIMP Draw() { return A rtistD raw (); } STDMETHOD(Arti stD raw )( ) =0; T im EwaJd showed me this technique originally, and Jim Springfield made me see its relevance to ATL. 225 了, INTERNALS struct _ICowboy : public ICowboy { STDMETHODIMP DrawC) { re tu rn CowboyOraw(); } STDMETHOO(CowboyDraw)() =0; }; Both _ I^ rti st and .ICowboy are shim classes that implement the method with the conflicting name and forward to another pure virtual member function with a u Kifjue name. Since both shims derive from the interface in question, the interfaces lA fti st and ICowboy can still appear in the interface map without difficulty: cla ss CAcePowel1 : publ ic CComOb jectRootlix , public —ICowboy, public _IArtist { p u b lic : BEGIN—COM—MAP(CAcePowel1) COM_INTERFACE一ENTRY(ICowboy) COM_INTERFACE_ENTRY(IArtist) END_COM_MAP() • •參 STDMETHODIMP A rtis tD ra w O ; STDMETHODIMP CowboyOrawO; }; This trick fills the vtbls for lA rti st and ICowboy with _ IA rti s t: :Draw and _ICowboy: : Draw. These functions, in tum, forward to the more derived class’s im­ plementation of the A rti stDraw and CowboyDraw. The forwarding shims remove our name conllict at the cost of an extra vtable per shim class, an extra entry per nu'thod per vtable,and an extra virtual function invocation per call. If this extra c ost bothers you, remove it using the standard ATL tricks: ^ template s tr u c t ATL—NO一VTABLE _IA rtist : public IA rtist { STDMETHODIMP Draw() { return static_cast(this) ->ArtistDraw(); } template s tru c t ATL一NO一VTABLE 一ICowboy : public ICowboy { 4 Don Box suggeste(this) ->CowboyDraw(); } c la s s ATL_NO_VTABLE CAcePowell : public CComObjectRootEx, public _ICowboy , public _IArtist { p u b lic : BECIN_COM_MAP(CAcePowel飞) LOM_INTERFACE_ENTRY(ICowboy) COM XNTERFACE„ENTRY(IArtist) END_COM_MAP() • • • HRESULT A r t is t D r a w O ; HRESULT C ow boyD raw O ; }; Don’t Go Off Half-Cocked... You may think it would be enough to change one of the names by only using one for­ warding shim: template struct ATL^NO^VTARLE —ICowboy : public ICowboy { STDMETHOOIMP Draw() { return stafic_cast(this) ->CowboyOraw(); > cla ss ATINO—VTABLE CAcePowel 1 : publ ic CComOb je c t Root Ex, p u b lic I A r t i s t { p u b lic : BECIN_COM_MAP(CAcePowell; COM_INTERFACE_ENTRY(ICowboy) CON..INTERFACE_ENTRY(IArti s t) END„COM_MAPC) HRESULT DrawC); / / Use fo r both I A r t i s t : : Draw and ICowboy: : Draw HRESULT CowboyDraw(); / / Never called! ATL INTERNALS • Don’t be tempted to try this. Remember that forwarding shims depend on over­ riding the behavior for the same member function name in the base classes. If you provide an implementation of the function in question with the same name as the function we’re implementing in the forwarding shim in the base, the forwarding shim function will never be called. By implementing one of the functions in the deriving class, you’ve effectively provided an implementation of both, putting you right back where you were in the first, place. Interface Coloring In the same “sneaky C++ trick” way that forwarding shims lets you fill the appro­ priate vtbl entries even if the compiler won’t cooperate, ATL supports another technique called interface coloring. Interface coloring is based on the idea that two classes can be layout compatible but not type compatible. For example, the following two classes are layout compatible, because they each result in a vtbl with the same number of methods and every method at the same offset has the same signature: struct ISphere : IUnknown { STDMETHOD(Rotate)(long nDegrees, long* pnOrientation) =0; STDMETH00(Twi r l) ( lo n g n V e lo c ity ) =0; struct IRedSphere { / / Colored IUnknown methods STDMETHOD(RedQuerylnterface)( REFIID riid , void** ppv) =0; STDMETHOD^CULONC, RedAddRef) ( ) =0; STDMETH00_(UL0NC, RedRelease)( ) =0; II Uncolored ISphere methods STDMETHOO(Rotate)(long nDegrees, long* pnOrientation) =0; STDMETHOD(Twirl)(long nVelocity) =0; However, because IRedSphere does not derive from ISphere, IRedSphere is not type compatible; that is, the compiler won’t let you pass IRedSphere where ISphere is expected (without coercion). Cloning the layout of an interface is known as interface coloring. The layout- compatible interface is said to be colored because it is identical to the original ex­ cept for the names, a feature not important to the runtime behavior of your object, just as a color is unimportant to the runtime behavior of your car. The names are INTERFACE MAPS used at compile time, though, and allow you to implement multiple versions of the same interface. For example: class CDesktopClobe : p u b li c CComObjectRootEx, public IRedSphere, public IG1obe, public IPlanet { BECIN_COM_MAP(CDesktopGlobe) / / Expose IRedSphere when ISphere is requested COM_INTERFACE_ENTRY_IID(IID_ISphere• IRedSphere) COM^INTERFACE_ENTRY( IG lobe) COM_INTERFACE_ENTRY(IPlanet) END_COM_MAP() • • • / / Colored method implementations STDMETHODIMP RedQueryInterface(REFIID riid , void** ppv) { return GetUnknown()->QueryInterfaceCriid, ppv); > STDMET ddR ef() { •here); return CetUnknownC)->Re1 easeO; > p r iv a te : long w_cRefSphere; By deriving from IRedSphere, we can provide an implementation of all of the col­ ored methods separately from the uncolored ones. By coloring the IUnknown meth­ ods of IRedSphere, we can handle IUnknown calls on ISphere separately from the other implementations of IUnknown by the other interfaces. In this case, w e ,re using RedAddRef and RedRel ease to keep track of an ISphe re-specific reference count. And even though we expose IRedSphere to the client when it asks for p u b lic _Thr _ C&m_cRefSphere); return CetUnknown()->AddRefQ; 230 ATL INTERNALS ISphere, as far as it’s concerned it just received an ISphere interface pointer. Since IRedSphere and ISphere are layout compatible, as far as COM is con­ cerned, the client is right. void TryRotateCIUnknown* punk) { ISphere* ps = 0; II Im plicit AddRef really a call to RedAddRef if ( SUCCEEDED(punk->QueryInterface(IID_ISphere, (void**)&ps) { // ps actually points to an IRedSphere* p s->R otate(); ps->Release(); // Really a call to RedRelease COM_INTERFACE_ENTRYJMPL and COM_INTERFACE_ENTRYJMPL_IID While interface coloring is somewhat interesting in the same way that a car wreck on the side of the road is interesting, it can also be disconcerting and may well slow down traffic. Beginning with ATL 3.0, interface coloring is no longer used by the vast majority of IXxxIm pl classes.5 In ATL 2.x, interface coloring was used for some, but not all, of the IXxxIm pl classes to perform interface-specific reference count­ ing. These implementation classes took the following form: template class IXxxImpl Instead of deriving from the interface the class implemented, the implementation class used interface coloring to make itself layout compatible with the implemented interface. This allowed each class to implement its own reference counting but pro­ hibited the use of the simple COM_INTERFACE_ENTRY macro. Instead, additional macros were provided to make the necessary entries in the interface map: fd e fin e ERFACE„ENTRY_JMPL(x) \ C0M.INT ENTRYJCID(IID—##x • x##Imp! <.CofnMapCl ass>) fdefine OmjCNTERFACE„ENTRY 1d, x) \ COM.INTERFACE«ENTR Y.I ID (1 i d • x##Imp1 <^CofnMapCl ass>) 6To my knowledge, only IProp€rtyPage2Impl still uses it However, since it uses interface coloring to return E^NOTIMPL, it doesn't seem too important INTERFACE MAPS 231 The interface coloring technique was only useful if you wanted to track refer­ ences on some of the ATL-implemented interfaces. Beginning with ATL 3.0,ATL uses a more generic mechanism6 that tracks reference counts on all interfaces. To­ ward that end, , public ISphere, public IRollableObject, public IPlaything, public ILethalObject, public ITakellpSpace, public Iw/ishlWereMoreUseful, public IfryToB€helpful, p u b lic I 灿 Depressed Because each beach ball implements eight interfaces, each instance has 32 bytes of overhead befoie the reference count or any useful state. If clients actually made heavy use of these interfaces, that wouldn’t be too high a price to pay. However, my guess is that most clients are going to use beach balls for their rollable and plaything abilities. Since the other interfaces w ill only be used infrequently, we’d rather not pay the overhead until they are used. For this, Crispin Goswell invented the tear-off interface, which he described in the “COM Programmer’s Cookbook,” available in the Microsoft Developer Network documentation.8 cThe _^\LT_DCBUG..INTERFACES macro provides this service and I discuss it in Chapter 3. 7Except for IProp*jrtyPage2Imp1,as I mentioned earlier »As of this writing, Crispin's article is also available online at http://www.microsoft.com/oIedpv/ ol(*com/com_co.htDTi. 232 ATL INTERNALS Standard Tear-offs A tear-off interface is an interface that we’d like to expose on demand, but not ac­ tually inherit from in the main class. Instead, an auxiliary class inherits from the tear-off interface and instances of that class are created any time a client queries for that interface. For example, assuming that few clients w ill think to turn a beach bali into a lethal weapon, ILethalO bject would make an excellent tear-off interface for the CBeachBal 1 class. Instead of using CComObj ectRootEx as the base class, ATL classes implementing tear-off interfaces use the CComTea rO f f Ob j e c t Base as their base class. template 〈class Owner, class ThreadModel « CCom0bjectThreadMode1> class CCo«TearOffObjectBase : p u b lic CComObjectRootEx { p u b lic : typ e d e f Owner 一 OwnerClass; / / BUG: The owner must be a CComObject. CGxnObject-cOwne^^ m_pOMner; CComTear0ff0bject8ase() { m_pOwner « NULL; } CComTearOffObjectBase provides one additional service, which is the cach­ ing of the owner of the tear-off interface. Each tear-off belongs to an owner object that has tom it off to satisfy a client’s request. The owner is useful so that the tear- off instance can access member data or member functions of the owner class. For example: class CBeachBal1 Lethal ness : public CCo«TearOffObjectBase, public ILethalObject { p u b lic : BECIN_COM_MAP(CBeachBal1 Lethal ness) COM_INTERFACE_ENTRYCILethalObject) END一COM_MAP() / / ILethalObject methods STDMETHODIMP K i l l O {DMETHOD: ■«pOwncr->«i_gasFin = GAS_HYDROGEN; n_pOwner->HoldNearOpenFl return S OK: DR ); 5 1 6 n INTERFACE MAPS 233 COMJNTERFACE_ENTRY_TEAR_OFF To make use of this tear-off implementation, the owner class uses the COM一INTER- FACE_ENTRY_TEAR_OFF macro: fd e fin e COH.INTERFACE.EKTRY.TEAfL.OFF( i i d . x) \ { A iid , \ ( DWORD)A«CComCreatorOata< \ CComInternalCreator< CComTearOffObject< x > > > ::data, \ •Creator }, The _CComC reato rData is just a sneaky trick to fill in the dw entry of the interface entry w ith a function pointer to the appropriate creator function. The creator func­ tion w ill be provided by CComlnternal C reator, which is similar to CComCreator except that it calls _In te rn a l Q uerylnterface to get the initial interface instead of Q uerylnterface. This is necessary because, as I’ll show you soon, Query- In te r f ace on a tear-off instance forwards to the owner, but we want the initial in­ terface on a new tear-off to come from the tear-off itself. That is, after all, why we’re creating the tear-off: to expose that interface. The pFunc entry made by COM_INTERFACE_ENTRY_TEAR_OFF is the first instance of a nonsimple entry so far in this chapter. The —Creator function is a static member of the CComOb j ectRootBase class that simply calls the creator function pointer held in the dw parameter s ta t ic HRESULT WINAPI CComObjectRootBase: :_Creator(void* pv, REFIID iid , v o id ** ppv, DWORD dw) { _ATL_CREATORDATA* pcd = (^ATL_CREATORDATA*)dw; return pcd->pFunc(pv, iid , ppv); } The most derived class of a tear-off implementation is not CComObject, but rather CComTearOffObject. CComT ea rO f f Ob j e ct knows about the nupOwner member of the base and w ill fill it during construction. Because each tear-off in­ stance is a separate C+ + object, each w ill maintain its own lifetime. However, to live up to the laws of COM identity, each tear-off w ill forward requests for new in­ terfaces to the owner template class CComTtarOffObject : public Base { p u b lic : CComTearOffObject(void* pv) { 234 ATL INTERNALS ATLA5SERT(m_pOwner == NULL); in^pOivner « reinterpret.cast*>(pv); m_pOwner->AddRef(); } -CComTearOffObjectO { m_dwRef ■ 1L; FinalReleaseO; ■_pOiR€leasc(); } STOMETHOO(Querylnterface)(REFIIO iid , void ** ppvObject) { return •_p(X*nep->Qu€ryInterfac«Ci'»d. ppvObject); } STOM£THOO.(ULONG, AddRef)( ) { return InternalAddRef(); } STOMETHOO.(ULONC, R elease)() { ULONC 1 * InternalReleaseC); i f ( l «a o) d e le te t h is ; re tu rn 1; To make use of a tear-off, the owner class adds an entry to its interface map: class CBeachBall : public CComObjectRootEx, public ISphere, public IRollableObject, public IPlaything, //public ILethalObject, / / Implevented by the tear-off public ITakeUpSpace, public IWishlWereMoreUseful, public ITryToBeHelpful, public IAmDepressed { p u b lic : BECIN_COM_MAP(CBeachBal1) COM一 INTERFACE_ENTRY(ISphere) COM_INTERFACE_ENTRYCIRollableObject) C0M_INTERFACE«ENTRY(IP1aything) C0MJCNTERFACE_ENTRY.TEAIL.OFF( IID J LethalO bj e c t• CBeachBal1 Lethal ness) INTERFACE MAPS 235 COM_lNTERFACE^ENTRY(ITakeUpSpace) COM—INTERFACE一ENTRY(IWishlWereMoreUseful) COM_INTERFACE_ENTRV(ITryToBeHelpful) COM_INTERFACE_ENTRY(IAmDepressed) END_COM_MAP() • • • p riv a te : CAS一 TYPE nugasFill; void HoldNearOpenFlame(); friend class CBeachBal1 Lethal ness; // Tear-offs are generally friends }; Since the owner class is no longer deriving from ILethalO bject, each instance is 4 bytes lighter. However, when the client queries for IL e th a l Ob j ect, we’re spend­ ing 4 bytes for the ILethalO bject vptr in CBeachBal ILethal ness, 4 bytes for the CBeachBal 1 Lethal ness reference count, and 4 bytes for the m_pOwner back pointer. You may wonder how spending 12 bytes to save 4 actually results in a savings. I ,ll tell you: volume! Or rather, the lack thereof. Since we're only paying the 12 bytes during the lifetime of the tear-off instance and we've used extensive profiling to determine that ILethalO bject is rarely used, the overall object foot­ print should be smaller. Be careful with that extensive profiling, though. You do need it if you’re going to be sure that you’re not paying more than you1 re saving when using tear-offs. Tear-off Caveats Before wrapping yourself in the perceived efficiency of tear-offs, there are some things of which you should be aware. ■ Tear-offs are only fo r rarely used interfaces. Tear-off interfaces are an im­ plementation trick to be used to reduce vptr bloat after extensive profiling has revealed this to be a problem. If you don’t have this problem, save yourself the trouble and avoid tear-offs. _ Tear-offs are for intra-apartment use only. The stub will cache a tear-off interface for the life of an object. In fact, the current implementation of the stub manager w ill cache each interface twice, sending the overhead of that particu­ lar interface from 4 bytes to 24 bytes. ■ Tear-offs should contain no state o f their own. When a tear~off contains its own state, there w ill be one copy of that state per tear-off instance, breaking the spirit if not the laws of COM identity. If you have per-interface state, especially large state that you would like to he released when no client is using the inter­ face, use a cached tear-off. 236 ATL INTERNALS Cached Tear-offs You may have noticed that every query for ILethalO bject will result in a new tear-off instance, even if the client already holds an ILethalO bject interface pointer. This may be fine for a single interface tear-off, but what about a related group of interfaces that w ill be used together?9 For example, imagine moving the other rarely used interfaces of CBeachBal 1 to a single tear-off implementation: class CBeachBallAttitude : public CComTearOffObjectBasef public ITakeUpSpace, public IWishlWereMoreUseful, public ITryToBeHelpful, public IAn^epressed { p u b lic : BECIN_COMJ«IAP(CBeachBall A ttitu d e ) COM_INTERFACE_ENTRY(ITakeUpSpace) COM_INTERFACE^ENTRY(IWishIWereMoreUseful) COM一 INTERFACE—ENTRY(ITryToBeHelpfu飞) COM_INTERFACE_ENTRY(IAmDepressed) END一COM—MAPO Although the following usage of this tear-off implementation will compile and exhibit the appropriate behavior, the overhead of even a single tear-off w ill be exorbitant: class CBeachBall : public CComObjectRootEx, public ISphere, public IRollableObject, public IPlaything { p u b lic : BEGIN一COM_MAP(CBeachBal1) COM一INTERFACE—ENTRY(ISphere) COM,INTERFACE_ENTRY(IRollableObject) C0NUINTERFACE-ENTRY(IP1aythi ng) 9 The control interfaces fit into this category for objects that also support nonvisual use. INTERFACE MAPS 237 C0M_INTERFACE_ENTRY_TEAR_0FF(IID_ILetha10bject, CBeachBallLethalness) COM„INTERFACE_ENTRY_TEAR_OFF(IIO-ITakeUpSpace, CBeachBal1Attitude) COM„INTERFACE_ENTRY_TEAR_OFF(IID_IWishlWereMoreUseful, CBeachBallAttitude) COM_INTERFACE.ENTRY- TEAR_OFF(IID_ITryToBeHelpful, CBeachBal1A ttitu d e ) COM_INTERFACE_ENTRY_TEAR_OFF(IID_IAiiDepressed, CBeachBal1A ttitu d e ) END_COM一 MAP() Because we've grouped the “attitude” interfaces together into a single tear-off im­ plementation, every time the client queries for any of them, it pays the overhead of all of them. To allow this kind of grouping but avoid the overhead of creating a new instance for every query, ATL provides an implementation of a cached tear-off. A cached tear-off is held by the owner if there is even one outstanding interface to the tear-off. The initial query w ill create the tear-off and cache it. Subsequent queries w ill use the cached tear-off. The final release w ill delete the tear-off instance. COMJNTERFACE 一 ENTRY_CACHED 一 TEAR_OFF To support caching tear-offs, ATL provides another interface macro: ♦define CO«- IMTERFACE.ENTRY.CAO«D- TEAIL.OFFCHd, x , punk) \ { & iid , \ (OWORD)A„CC acheData< \ CComCreat_ CCowCachedTearOffObjcct < x > >, \ (DWORD)offsetofCCon^apCIass, punk) > ::datat \ -Cache }• The _CComCacheData class is used to stuff a pointer into an _ATL_CACHEDATA structure: s tru c t JVTL.CACHEOATA { DWORD cM)ffsetVar; ^MI^CREATORFUNC* pFunc; The use of this structure allows the dw to point to a creator function pointer as well as another member, an offset. The offset is from the base of the owner class to the member data that is used to cache the pointer to the tear-off. The _Cache func­ tion, another static member fimction of CComObj ectRootBase, uses the offset to ATL INTERNALS calculate the address of the pointer and checks the pointer to determine whether to create a new instance of the cached tear-off: s t a t ic HRESULT WINAPI CComObjectRootBase::_C»ch«(vo1d* pv, REFIID iid , v o id ** ppvO bject, DWORD dw) { HRESULT hRes • E.JWINTERFACE; _ATL.CACHEDATA* pcd * CATL_CACHEDATA” dw; IUnknown** pp « (IUnknown**)( (DWORD)pv ♦ pcd*>(M)ffs«tV«r); I f (#pp «• NULL) NRes ■ pcd->pFuncCpv. IIDJUnknown, (vo1d**)pp); 1 f (*pp ! * NULL) HRes • (*pp)->0u®rylnt«rf«c«(11d, ppvObj«ct); re tu rn hRes; Just as an instance of a tear-off uses CComTearOffObject instead of CCom­ O bject to provide the implementation of IUnknown, cached tear-offs use CCom- CachedTearOffObject. CComCachedTearOffObject is nearly identical to CComAggObject10 because of the way that the lifetime and identity of the tear-off are subsumed by that of the owner. The only difference is that the cached tear-off, like the tear-off, w ill initialize the m_pOwne r member. Replacing the inefficient use of C0M_INTERFAC已 ENTRY—TEAR-OFF w ith COM_INTERFACE_ENTRY_CACHED_TEAR_OFF looks like this: class CBeachBall : p u b lic CComObjectRootEx, public ISphere, public IRollableObject, public IPlaything { p u b lic : BEGIN_COH_MAP(CBeachBal1) COM^INTERFACE_ENTRY(ISphere) COM一INTERFACE一ENTRY(IRol1ableObject) COM一INTTERFACE_ENTRY(IP1 a y th i ng) COM_INTERFACE_ENTRY.TEAR_OFF(IID.ILethalObject, CBeachBallLethal ness) COMJNTERFACE.ENTRY.CACHEO.TEAIUOFFCIID^rrakeUpSpace, CBeachBall A ttitu d e , RL.spunkAtti tude. p) COM_INTERFACE_ENTRY^CACHED_TEAR_OFF(IID«IW1shlWereMoreiJseful, CBeachBallAttitude, «_spunkAttitude.p) 10 Discussed in Ch^)ter 3. INTERFACE MAPS 239 COM_INTERFACE_ENTRY_CACHEO.TEAR_OFF(IID_ITryToBeHelpful, CBeachBal1A ttitu d e , m一 spunkAtti tude.p) COM_INTERFACE_ENTRY_CACHED_TEAR- OFFCIID_IAmOepressed, CBeachBal1A ttitude, END_COM._MAP() DECLAREJET-CONTROLLING—UNKNOWN() / / See the A ggregation s e c tio n p u b lic : CComPtr m.spunkAttitude; Another Use for Cached Tear-offs Cached tear-offs have another use that is in direct opposition to standard tear-offs: caching per-interface resources. For example, imagine a rarely used IHyphena- t i on interface: interface IHyphenation : public IUnknown { HRESULT Hyphenate([in] BSTR bstrUnhyphed, [out, retval] BSTR* pbstrHyphed); > ; Performing hyphenation is a matter of consulting a giant lookup table. If a C D ictionary object were to implement the IHyphenation interface, it would likely do so as a cached tear-off to manage the resources associated w ith the lookup table. When the hyphenation cached tear-off was first created, it would acquire the lookup table. Since the tear-off is cached, subsequent queries would use the same lookup table. Once all references to the IHyphenation interface were re- leaseti. the lookup table could be released. If we had used a standard tear-off for this same functionality, a naive implementation would have acquired the resources for the lookup table for each tear-off. Aggregation: The Controlling Outer Like tear-offs, aggregation allows you to separate the code for a single identity into multiple objects. However, whereas using tear-offs requires shared source code be­ tween the owner and the tear-off class, aggregation does not. The controlling outer and the controlling inner do not have to share the same server or even the same implementation language (although they do have to share the same apartment). If 240 ATL INTERNALS you like, you can consider an aggregated object 汪 kind of “binary cached tear-off." Just like a cached tear-off, an aggregated instance’s lifetime and identity are sub­ sumed by that of the controlling outer. Just like a cached tear-off, an aggregated instance must have a way to obtain the interface pointer of the controlling outer. In a tear-off, we pass the owner as a constructor argument. In aggregation, we do the same thing, but using the COM constructor that accepts a single, optional constructor argument, that is, the pUnkOuter parameter of IC lassF actory:: Createlnstance and its wrapper, CoCreatelnstance. interface IClassFactory : IUnknown { HRESULT C reatelnstance([in% unique] IUnknown* pUnkOuter, [in] REFIID riid . [out, iid_is(riid)3 void **ppvObject); HRESULT LockServer([in] BOOL flo c k ); WINOLEAPI CoCreatelnstanceC[ i n] [in, unique] [ in ] [ in ] [out, iid_is(riid)] REFCLSID r c ls id , LPUNKNOWN pUnkOuter, OWORO dwClsContext, REFIID riid . LFV0I0 FAR* ppv); In Chapter 3 ,1 discussed how ATL supports aggregation as a controlled inner using CComAggOb je c t (or CComPol yOb j ect). In this chapter, I’ll show you the four macros that ATL provides to allow you to be the controlling outer in the aggregation relationship. Planned versus Blind Aggregation Once an aggregate is created, the controlling outer has two choices about how to expose the interface(s) of the aggregate as its own. The first choice is planned ag­ gregation (Figure 5.2). Planned aggregation means that the controlling outer only wants the inner to expose one of a set of interfaces known by the outer. The down­ side to this technique is that, if the inner’s functionality grows, clients using the outer w ill not gain access to the additional functionality. The upside is that this may well be exactly what the outer had in mind. For example, consider the standard interface IP ersi st: interface IPersist : IUnknown { HRESULT C ctC lassIO C [out] CLSID *p C la s s I0 ); INTERFACE MAPS 0 Controlling Outer lOuter 〇 I Inner 1 :—) Controlled Inner IlnnerN ( Figure 5.2. Planned aggregation If the outer were to blindly expose the in n e r ,s implementation of IP ersi st, when the client called GetCI ass ID, it would get the class identifier (CLSID) of the inner, not the outer. Since the client is after the outer object’s CLSID, we have yet again broken the spirit of the COM identity laws. Planned aggregation helps us to prevent this breach. Blind aggregation, on the other hand, allows the outer’s functionality to grow with the inner’s but provides the potential for exposing identity information from the inner (Figure 5.3). For this reason, blind aggregation should be avoided. Manual versus Automatic Creation COMJNTERFACE 一 ENTRY一 AGGREGATE and COM.INTERFACE_ENTRY_AGGREGATE_BLIND ATL provides support for both planned and blind aggregation via the following two macros: •d e fin e C0M.INTERFACE.ENTRY.ACCRECATEC11d, punk) \ { A iid, (OWORO)offsetofCComMapClass, punk)f ^Delegate }• fd e fin e COICINTERFACE_ENTRY^ICCRECATE,BLIND(punK) \ { NULL. (OWORO)offsetofC-ComMapClass, punk)f ^D elegate}, 242 ATL INTERNALS lOuter > llnnerl f ) IlnnerN a s Controlling Outer c Controlled Innerr \) Figure 5.3. Blind aggregation These macros assume that the aggregate has already been created manually and that the interface pointer is stored in the punk parameter to the macro. The _Del egate function forwards the Q u e ryln te rf ace request to that pointer Static HRESULT WINAPI CComObjectRootBase::J)«le 9at€(void# pv, REFIID iid , v o id ** ppv, DWORO dw) { HRESULT hRes * £_NOINTERFACE; IUnknown* p » IUnknown** )((DWORO)pv ♦ dw); if (p !» NULL) hRes - p->Qu«ryInt«rfac«C,l1d, ppv); return hRes; | } To aggregate an inner that has been manually created, a class uses COM一INTER- FACE_ENTRY«_AGGRECATE o r COM_INTERFACE_ENTRY_ACGRECATE_BLIND in the interface map: class CBeachBal1 : public CComObjectRootEx re tu rn h r; } void FinalReleaseO { w_spunkLethal ness. Rel easeO ; m^spunkAtti tude.Release(); > p u b lic : CComPtr m_spunkLethalness; CComPtr m_spunkAttitude; 244 ATL INTERNALS Notice that I have used the Fi nal C onstruct method to create the aggregates so that failure w ill stop the creation process. Notice also that because I have a Fi nal • C onstruct that hands out interface pointers to the object being created, I’m using DECLARE一 PROTECT一 FINAL—CONSTRUCT to protect against premature destruction. And because the aggregates have pointers to the outer, I have a Fi nal Rel ease method to manually release the aggregate interface pointers to protect against double destruction. Aggregation was one of the chief motivations behind the multi­ phase construction of ATL base COM objects, so it’s not surprising to see all of the pieces used in this example. However, one thing I have not yet mentioned is the DECLARE_CET_CONTROL- LING_UNKNOWN macro. The controlling unknown is a pointer to the most outer con­ trolling outer. Because aggregation can go arbitrarily deep, the outer needs to pass the pUnkOuter of the most outer when aggregating. To support this, ATL provides the GET_CONTROLLING_UNKNOWN macro, which gives an object the definition of a CetControl 1 i ngllnknown function: #define DECLARE_CET_CONTROLLINC_UNKNOWNC) public: \ vi rtual IUnknown* CetControl1ingUnknown() { return CetUnknown(); } You may question the value of this function, since it simply forwards to Get- Unknown, but notice that it’s virtual. When the object is actually being created as an aggregate while it is aggregating, GetCont ro l 1 i ngUnknown w ill be overridden in CComContai nedObject like so: tem plate cla ss CComContai nedObject : p u b lic Base { • • • IUnknown-^ CComContai nedObj e c t :: CetCont ro l 1 i ngUnknown () { return m_pOuterlinknown; } So, if the object is standalone, CetControl 1 ingUnknown w ill return the IUn­ known* of the object, but if the object is itself being aggregated, GetCont ro l - 1 i ngUnknown w ill return the IUnknown* of the outermost outer. COM」 NTERFACE_ENTRY—AUTOAGGREGATE and COMJNTERFACE_ENTRY^AUTOAGGREGATE_BLIND When you’re not initializing any of the aggregates, it seems a waste to create them until, or unless, they’re needed. For automatic creation of aggregates on demand, ATL provides the following two macros: INTERFACE MAPS 245 # d e fin e C0M_INTERFACE.ENTRY_AUT0ACCRECATE(1id, punk, c ls id ) \ { & iid , \ (OWORO)&_CComCacheData< \ CComAggregateCreator<_ComMapC1 ass, 5fClsid>, \ (OWORO)offsetof(_ComMapClass, punk)>: : data•\ —Cache }• ♦ define COM^INTERFACE.ENTRY.AUTOACXlRECATE^BLINOCpunk. c ls id ) \ { NULL, \ (OWORD)&_CComCacheData< \ CComAggregateCreator<_ComMapC1ass, &clsid>. \ (DWORD)offsetofCComMapClass, punk)>::data A -Cache }, The only new thing in these macros is the CComAgg regateC reator, which simply performs the CoCreatelnstance for us the first time the interface is requested: template class CCowAggregateCreator { p u b lic : sta tic HRESULT WINAPI CreatelnstanceCvoid* pv, REFIID, LPVOID* ppv) { ATlASS£RT(*ppv NULL); ATLASSERT(pv (« NULL); T* p • (T *)p v ; return CoCrcateInstance(*pclsid, p->CetContromngUnknown(), CLSCTK_INPROC, XID_IUnknown, ppv); Using automatic creation simplifies the outer’s code somewhat: class CBeachBall : publlc CComObjectRootEx, public ISphere, public IRollableObject, public IPlaything { p u b lic : BECIN_COM_MAP(CBeachBal 1) COM_INTERFACE_ENTRY(ISphere) 246 ATL INTERNALS COH^INTERFACE^ENTRY(IRonableObject) C0W_INTERFACE_ENTRY(IP1 a y th i ng) COM_INTERFACEwEMTRY_AUTOAGCREGATE(IIDJLethal Obj ect, ■_spunkLctha1ness•p• CLSID_Letha1ness) COILJNTERFACE^ENTRYJMJTOACGRECATE^BLINOC^spunkAtti tud« • p , CLSIDJ^ttitude) END_COW_MAP() DECLARE^CET_CONTROLLINC>UNKNOWN() void FinalReleaseO { nuspunkLethalness•ReleaseC); m_spunkAttitude•Release O ; > • • • p u b lic : CComPtr m_spunkLetha1ness; CComPtr m_spunkAttitude; Although we no longer need to perform the creation in Final Construct, we’re s till required to use DECLARE_GET_CON丁ROLLING_UNKNOWN and to provide storage for the aggregated interfaces. We should still release the interfaces manually in Fi nal Rel ease, as well, if we're interested in avoiding double destruction. Aggregating the FrM-Threaded M丨rshaler One particularly interesting use of aggregation is supported directly by the ATL Ob­ ject Wizard: aggregating the implementation of IM arshal provided by the Free- Threaded Marshaler (FTM). Any object that aggregates the FTM is said to be an apartment-neutral object. Normally, passing an interface pointer between 叩 art- merits, even in the same process, results in a proxy/stub pair. While the proxy/stub pair maintains the concurrency and synchronization requirements of both the ob­ ject and the client, it also adds overhead. In-process objects that provide their own synchronization and prefer to snuggle up to the client without the overhead of the proxy/stub can aggregate the FTM. By aggregating the FTM, an object is short- circuiting the creation of the proxy/stub, and therefore the object can be passed be­ tween bailm ents in the same address space without the overhead of a proxy/stub. The following code is generated by the wizard when the Free-Threaded Mar­ shaler option is checked in the ATL Object Wizard: cla ss ATL.NO^VTABLE CBow lingBall : p u b lic CCotnOb je c t Root Ex , INTERFACE MAPS 247 public CComCoClass, public IBowlingBall { p u b lic : CBowlingBalU) { m^pUnkMarshaler =« NULL; > D£ClARE_RECISTRY.RESOURCEID(IDR_BOWLINCBALL) DECLARE.NOTJ^GCRECATA8LE(CBowlingBal1) D£CLARE.CET_CONTROLLINC_UNKNOWN() DECLARE.PROTEa.FINAL^CONSTRUaO BECIN.COM^MAPCCBowlingBal1) COM.INTERFACE.EMRYCIBowl i ngBal 1) COM«INTERFACE.ENTRY^ACCRECAT£(IIO^IMarshal, m.pUnkMarsha1er.p) ENO^COM_MAP() HRESULT FinalConstruet() { return CoCreateFreeThreadedMarshalerCCetControl1ingUnknownO, ta^pUnkMarsbal€r.p); > void FinalReleaseO { ai^pUnkMarshaler.Release(); > CCoMPtr n^>UnkMarshaler; Because the CLSID of the FTM is not available, instead of using auto-creation, ATL uses CoCreateFreeThreadMarshaler to create an instance of the FTM in the Fi nalConstruct method. FTM Danger, Will Robinson! Danger! Danger! Since aggregating the FTM is so easy, I should mention a couple of big responsibil­ ities that you, the developer, have accepted by aggregating the FTM. Apartment-neutral objects must be thread safe. You can mark your class ThreadingModel =Apartment all day long, but because your object can be passed freely between apartments in the same process and therefore used simultaneously by multiple threads, you had better use CComMul tiT h re a d ­ Model and at least object-level locking. Unfortunately, the same wizard that ATL INTERNALS makes it so easy to use the FTM is also perfectly willing to let you choose the Single or the Apartment threading models. I suggest using Both to avoid the spurious creation of an extra apartment. ■ Apartm ent-neutral objects are not aggregatable. Aggregating the FTM de­ pends on being able to implement IM arshal. If the outer decides to implement IM arshal and doesn’t ask the inner object, the inner can no longer be apart­ ment neutral. ■ Apartment-neutral objects may not cache interface pointers. An apart­ ment-neutral object is said to be apartment neutral because it doesn’t care from which apartment it is accessed. However, other objects used by an apartment- neutral object may or may not also be apartment neutral. Interface pointers to objects that aren’t apartment neutral can only be used in the apartment to which they belong. If you’re lucky, the apartment-neutral object attempting to cache and use an interface pointer from another apartment will have a pointer to a proxy. Proxies know when they are being accessed outside of their apartments and w ill return RPC_E_WRONG一THREAD for all such method calls. If you’re not so lucky, the apartment-neutral object w ill obtain a raw interface pointer. Imagine the poor single-threaded object accessed simultaneously from multiple apartments as part of its duty to the apartment-neutral object. It’s not going to last long. The only safe way to cache interface pointers in an apartment-neutral ob­ ject is as cookies obtained from the Global Interface Table (GIT). The GIT is a process-global object provided to map apartment-specific interface pointers to apartment-neutral cookies and back. The GIT was invented after the FTM and is provided in the third service pack to NT 4.0. the DCOM upgrade for Windows 95,and out of the box with Windows 98. If you’re aggregating the FTM and caching interface pointers, you must use the GIT. For an in-depth discussion of the FTM, the GIT, and their use, read Essential COM by Don Box (Addison-Wesley, 1998). Interface Map Tricks Interface Map Chaining C++ programmers are accustomed to code reuse via inheritance of implementa­ tion. For example, inheritance is how we reuse the implementation provided in CComObj ectRootEx as well as the various ATL implementation classes, for ex­ ample, ID i spatchlm pl. For each implementation class used, one or more corrc- INTERFACE MAPS 249 sponding entries must be made in the interface map. However, what about deriving from a class that already provides an interface map? For example: class CBigBeachBall : public CBeachBall, public IBigObject { p u b lic : BEGIN_COM_MAPCCBigBeachBal1) COM_INTERFACE_ENTRY(IBigObject) I I A ll entries from CBeachBal1 base? END_COM_MAP() COMJNTERFACE_ENTRY_CHAIN What we’d like to avoid, when inheriting from a base class that provides its own in­ terface m a p ,is duplicating all the entries in the deriving class’s interface map. The reason is maintenance. If the base class decides to change how it supports an in­ terface or to add or remove support for an interface, we have to change the deriv­ ing classes, too. It would be much nicer to be able to “inherit” the interface map along with the interface implementations. That’s what COM_INTERFACE_ENTRY_ CHAIN is for. •d e fin e C0M.INTERFACE.ENTRYmCHA1M(c1 assnaM) \ { NULL, (DWORD)&_CComChainData: :datay \ •Chain }• The _CComChai nData template simply fills the dw member of the interface entry with a pointer to the base class’s interface map so that the _Chai n function can walk that list when evaluating a query request: s ta tic HRESULT WINAPI CComObjectRootBase: :.Om1n(void# pv, REFIIO iid , void** ppvObject, DWORD dw) { •ATUCHAINDATA* pcd « CATUCHAINDATA*)dw; void* p • (void*)((OMORO)pv ♦ pcd->dMOffs«t); return Int«rvui1QiMryInt«rfac^Cp, pcd>>pFuncO, 11d, ppvObjcct); > 250 ATL INTERNALS When the _Chai n function returns a failure—for example, the base class doesn’t support the requested interface—the search continues with the next entry in the table. For example: class CBigBadBeachBal1 : public CBeachBall, public IBigObject, public IBadObject { p u b lic : BECIN_COM_MAP(CBigBadBeachBal1) COM_INTERFACE_ENTRY(IBigObject) COM_INTERFACE_ENTRY_CHAIN(CBeachBal1) COM_INTERFAC£_ENTRY(IBadObject) END一 C0M_MAP() It may seem natural to make the chaining entries first in the interface map. However, remember that the first entry must be a simple entry, so put at least one of the derived class’s interfaces first. If the derived class has no additional inter­ faces, use IUnknown as the first entry: class CBetterBeachBall : public CBeachBall { p u b lic : BECIN_COfLMAP(CBetterBeachBal1) amjWTERFACE»EMTRY(IUnknown) CO»LINTERFACE.ENTRY.CHAINCCB€achBal 1) END_C0M_HAP() Just Say "No” COMJNTERFACE 一 ENTRY-NOINTERFACE Sometimes you,d like to short-circuit the interface request by explicitly returning E_N0INTERFACE when a specific interface is requested. For this, ATL provides COM_INTERFACt_ENTRY_NOINTERFACE: #define COMJPTTERF/W:E^ENTRY.NOII, The _Break function outputs some helpful debugging information and calls DebugBreak: s ta tic HRESULT WINAPI CComObjtctRootbasc:REFIID iid , void**. OMORO) { 252 ATL INTERNALS JVTLOUMPIIDCiid, _T("Break due to QI for interface ••), S_OK); Debug8reakC) ; re tu rn S_FALSE; The call to DebugBreak is just like a breakpoint set in your debugger. It gives the active debugger the chance of taking control of the process. Once you’re debugging, you can set other breakpoints and continue executing. Extensibility COM_INTERFACE_ENTRY_FUNC and COMJNTERFACE 一 ENTRY 一 FUNC 一 BUND ATL provides two macros for putting raw entries into the interface map: •d e fin e C0«.IWTERFACE_ENTRY.FUNCC1id, dw, fu n c) \ { A iid . dw, func }• •d e fin e CX)H„INTERFACEwENTRY_FUNC.BLINO(dw. fu n c) \ { NULL, dw, func }• These macros are the universal backdoor to ATL’s iinplementation of Query­ ln te rfa c e . If you come up with another way of exposing COM interfaces, you can use these macros to achieve it, as long as it lives up to the laws of COM identity. Direct Access to the this Pointer One identity trick you can perform using COM_INTERFACE_ENTRY一 FUNC was kicked around the ATL mailing list for quite a while, but was recently perfected by Don Box (a slightly modified version of his solution is provided below). In Ch叩 te r 3, as you may recall, I presented the C reatelnstance static member functions of CComObject, CComAggObject, and CComPolyObject when using private initial­ ization. The C reatelnstance method performed the same job as a creator would, but returned a pointer to the th i s pointer of the object instead of only to one of the interfaces. This was useful for calling member functions or setting member data not exposed via interfaces. We used this technique because it was unsafe to perform a cast. However, why not make Q uerylnterface perform the cast safely? In other words, why not add an entry to the interface map that returns the object’s th i s pointer? Imagine a global function with the following implementation: INTERFACE MAPS 253 i n l i ne HRESULT WINAPI _This(void* pv, REFIID iid . void** ppvObject, DWORD) { ATLASSERT(iid == IID.NULL); *ppvObject = pv; re tu rn S一OK; This function takes the first parameter, pv, which points to the objects th is pointer and hands it out directly in ppvObject. Notice also that this function does not AddRef the resultant interface pointer. Since it’s returning an object pointer, not an interface pointer, it’s not subject to the laws of COM. Remember, the th i s pointer is only useful within the server. To make sure that any out-of-apartment calls to Q uerylnterface fail, be sure to pick an interface ID without a proxy/stub, for example, IID^NULL. For example, imagine implementations of the following interfaces, creating a simple object model: interface IBalloonMan : IUnknown { HRESULT CreateBal1oon(1ong rgbColor, IBalloon** ppBalloon); HRESULT SwitchColorCIBalloon* pBalloon, long rgbColor); > ; interface IBalloon : IUnknown { [propget] HRESULT Color([out, retval] long *pVal); Notice that the balloon's color can't be changed via IB alloon, but the implemen­ tation of IB al 1 oonMan is able to give you a balloon of the color you’re after. If the implementations of IB al 1 oonMan and IB alloon share the same server, the im­ plementation of IB alloon can expose its th is pointer via the 一This function like so: cla ss ATL_NO_VTABLE CBalloon : p u b l i c CComObj ectRootEx, p u b lic CComCoClass, public IBalloon { p u b lic : DECLARE_REGISTRY_RESOURCEID(IDR_BALLOON) DECLARE_NOT_AGCREGATABLE(CBal1oon) ATL INTERNALS DECLARE^PROTEa^FINAL_CONSTRUCT() BECIN^COM_MAP(CBanoon) COM_INTERFACE_ENTRY(IBal1oon) COM„INTERFACE.ENTRY_FUNCCIID_NULL, 0, .T h is ) EN0.C0M_MAP() // IBalloon p u b lic : STDMETHOO(get_Color)( /* [out, re tva l]*/ long *pVa1) p riv a te : COLORREF m_rgbCo1or; friend class CBalloonMan: Because CBal 1 oonMan is a friend of CBal ^ oon, CBal 1 oonMan can set the pri­ vate color data member of CBal loon, assuming it can obtain the object’s th i s pointer. CBal 1 oon’s special entry for IID_NULL lets CBal 1 oonMan do just that: STOMETHODIMP CBalloonMan: :CreateBalloon(long rgbColor, IBalloon** ppBalloon) { II Create balloon HRESULT hr =» CBalloon: :CreateInstance(0, ppBalloon); i f ( SUCCEEDED(hr) ) { / / Use backdoor to acquire CBal1oon's this CBalloon* pBalloonThis = 0; hr = CppBalloon)->QueryInterfaceCIIO«NULL * i f ( SUCCEEDED(hr) ) { II Use CBalloon's this pointer for private pBalloonThis->m_rgbCo1or = rgbColor; } > (void**)&pBal1oonThis) initialization if( FAILED(hr) ) { C*ppBal1oon)->Release(); ♦ppBalloon * 0; > re tu rn h r; INTERFACE MAPS 255 The benefit of this technique over the private initialization technique exposed by the C reatelnstance member function of CComObject et al. is that the th i s pointer can be obtained after creation. For example: STDMETHODIMP CBalloonMan: : SwitchColorCIBalloon* pBalloon, long rgbColor) { // Use backdoor to acquire CBalloon's this pointer CBalloon* pBal1oonThi s = 0; HRESULT hr = pBa11oon->QueryInterface(IID.NULLt (void**)&pBalloonThis); i f ( SUCCEEDEDChr) ) { pBal1oonThi s->m_rgbCo1or = rgbColor; > re tu rn h r; } Clearly, this technique is a backdoor hack with limited usefulness. It should not be used to subvert the binary boundary between client and object. However, it does have its own special charm. For objects that share the same 叩 artment in the same server, it s a valid way to discover just who is who. If you find yourself using this technique, you may find the following macro to be a useful shortcut: #de fin e COM_INTERFACE.ENTRY_THIS() \ COM_INTERFACE_ENTRY^FUNC(IID_NULL, 0, —Th is) Per-Object Interfaces Sometiraes it’s useful to handle interfaces on a per-object basis instead of a pcr- class basis. Another friend of mine, Martin Gudgin, provided the following example. If you’d like to implement something known as a “smart proxy,” you’re going to have to keep track of the list of interfaces each object supports, and you may not know what those are until runtime. Each smart proxy object has its own list of interfaces, which can easily be managed by a member function. Unfortunately, interface maps can't hold member functions (believe me, I tried). However, you can use a combi­ nation of CONLINTERFACE_ENTRY_FUNC_BLIND and a static member function to perform the forwarding to a member function. For example: class ATIN0_VTABLE CSmartProxy : public CComObjectRootEx, public CComCoClass, public IUnknown ATL INTERNALS p u b lic : DECLARE_RECISTRY_RESOURCEID(IDR_SMARTPROXY) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CSmartProxy) COM_INTERFACE_ENTRY(IUnknown) COM一 INTERFACE_ENTRY_FUN(LBLIND(0, 一 QI) END一 COM一 MAP() p u b lic : s t a t ic HRESULT WINAPI _ Q I(v o id * pv, REFIID iid , void** ppvObject, DWORD) { / / Forward to QI member function return ( (CSmartProxy*)pv)->QI(i id , ppvObject); } // Per-object implementation of QI HRESULT QI(REFIID riid , void** ppv); Of course, you may wonder why you’d go to all this trouble to get back to the per- object implementation of Q uerylnte r f ace that you’ve come to know and love, but that’s ATL. Summary Even after you understand and commit to the laws of COM identity, you il find that they aren’t very restrictive. Prefer multiple inheritance, but do not feel that ATL lim­ its you to that technique. And for any identity trick that ATL doesn’t support di­ rectly. the interface map can be extended to support it. CHAPTER 6 Persistence in ATL A Review of COM Persistence Objects t hat have a persistent state of any kind should implement at least, one per­ sistence interface, and preferably multiple interfaces, in order to provide the con­ tainer with the most flexible choice of how it wishes to save the object’s state. Persistent state is that date (typically properties and instance variables) that an ob­ ject needs preserved before a container destroys the object. The container provides the saved state to the object after recreating the object so the object can reinitialize itself to its previous state. COM itself doesn't require an object to support persistence, nor does COM use such support if it’s present in an object. COM simply documents a protocol by which clients may use any persistence support provided by an object. Often, we refer to this persistence model as client-managed persistence because it is the client that determines where the persistent data (the medium) is saved and when the save occurs. COM defines some interfaces that model a persistence medium and, for some media, an implementation of the interfaces. Such interfaces typically use the nam­ ing convention I Medium, where Medium is Stream, Storage, PropertyBag, and so on. The medium interface has methods such as Read and Wri te, which an object uses when loading and saving its state. COM also documents interfaces that an object implements when it wishes to support persistence into various media. Such interfaces typically use the naming convention IP e rs i stMec/f urn. The persist interface has methods such as Load and Save, which the client calls to request, the object to restore or save its state. The client provides the appropriate medium interface to the object as an argument to the Load or Save request. Figure 6.1 illustrates this model. All IP e r s istMedium interfaces derive from the IP e rs is t interface, which looks like this: in te rfa c e IP e rs is t : IUnknown { HRESULT v>tC1assI0([out] CLSID* pclsid); > 257 AT L 丨NTEAMALS \Pers\s\Medium ♦ Load Save Object Persistence Media I Medium Read Write Persistent State Figure 6.1. The client-managed persistence model A client uses the G etClassID method when it wishes to save the slate of an object. T>pically, the client queries for IP e rsi stMedium, calls the GetClassID method to obtain the class identifier (CLSID) for the object the client wishes to save, then writes the CLSID to the persistence niedium. The client then requests the object to save its state into the medium. Restoring the object is the inverse operation: Road the CLSID from the medium. Create an instance of the class. Query for the IP e r­ s i stl^edi um interface on that object. Request the object to load its state from the medium. There are two basic forms in which a client might ask an object to save its slate: a self-describing set of named properties or an opaque binary st ream of bytes. When an object provides its state as a self-describing set of named properties, it provides each property as a name/type/value tuple to its client. The client then stores the properties in the form most, convenient to the client, for example, text on HTML pages. The benefit of self-describing data, for example, tags and XML, is that one entity can write it and another can read it without tight coupling between the two. It is more efficient for an object to provide its state as a binary stream of bytes because the object doesn't need to provide a property name or translate each prop­ erty into the name/type/value tuple. AJso, the client doesn’t need to translate the PERSISTENCE IN ATL property to and from text. However, opaque streams w ill contain machine depen­ dencies, such as byte order and floating point/character set representations, unless specifically addressed by the object writing the stream. ATL provides support for both forms of persistence, but before we explore ATL’s persistence implementation, let’s look at how you might implement COM per­ sistence directly. IPropertyBag and IPersistPropertyBag ActiveX control containers that implement a save as text mechanism typically use IPropertyBag and IP ersi stPropertyBag. A container implements IProper­ tyBag and a control implements IP ersi stPropertyBag to indicate that it can persist its state as a self-describing set of named properties. interface IPropertyBag : public IUnknown { HRESULT Read ( [ i n ] LPCOLESTR pszPropName, [out][in] VARIANT* pVar, [in] IErrorLog* pErrorLog); HRESULT W rite ( [ i n ] LPCOLESTR pszPropName, [ in ] VARIANT* p V a r); interface IPersi stPropertyBag : public IPersist { HRESULT InitN ew ( ) ; HRESULT Load ([in ] IPropertyBag* pPropBag, [in] IErrorLog* pErrorLog); HRESULT Save ([in ] IPropertyBag* pPropBag, [in ] BOOL fClearOirty, [in ] BOOL fSaveAllProperties); When a client (container) wishes to have exact control over how individually named properties of an object are saved, it attempts to use an object’s IP e rs i s t­ PropertyBag interface as the persistence mechanism. The client supplies a prop­ erty bag to the object in the form of an IPropertyBag interface. When the object wishes to read a property in IPersistPropertyBag:: Load, it w ill call IP ropertyB ag::Read When the object is saving properties in IPersi stPropertyBag: :Save, it will call IPropertyBag::W rite. Each prop­ erty is described with a name in pszPropName whose value is exchanged in a VARIANT. For read operations, the property bag provides the named property from the bag in the form specified by the input VARIANT, Unless the type is VT_EMPTY, in 260 ATL INTERNALS which case, the property bag provides the property as any VARIANT type conve­ nient to the bag. The information provided by the object for each property (name/type/value) during a save operation allows a client to save the property values as text, for in­ stance, which is the primary reason why a client might choose to support IP e r­ si s t Property Bag. The client records errors that occur during reading into the supplied error log. I Property Bag2 and IPersistPropertyBag2 The I PropertyBag interface doesn’t give an object much information about the properties contained in the bag. Therefore, there is a newer interface, IP rope r ty - Bag2, which allows an object much greater access to information about the prop­ erties in a bag. Objects that support persistence using IPropertyBag2 naturally implement the IP ersi stPropertyBag2 interface. interface IPropertyBag2 : public IUnknown { HRESULT Read ( [ i n ] ULONC 〔Properties, [in ] PR0PBAC2* pPropBag, [ in ] IE rro rL o g * pErrLog, [o u t] VARIANT* pvarvalue [o u t] HRESULT* p h rE rro r); HRESULT Wri te ( [ i n ] ULONC c P ro p e rtie s , [ in ] PR0PBAC2* pPropBag, [in] VARIANT* pvarValue); HRESULT CountProperties ([out] ULONC* pcProperties); HRESULT CetPropertylnfo ([in ] ULONC iProperty, [in ] ULONC cProperties, [o u t] PR0PBAC2* pPropBag, [out] ULONC* pcProperties); HRESULT LoadObject ( [ i n ] LPCOLESTR pstrName, [ in ] DWORO dwHint, [in ] IUnknown♦ pUnkObject, [in ] IErrorLog' pErrLog) interface IPersistPropertyBag2 : public IPersist { HRESULT InitN e w ( ) : HRESULT Load ([in ] IPropertyBag2* pPropBag, [in] IErrorLog* pErrLog); HRESULT Save ([in ] IPropertyBag2* pPropBag, [in ] BOOL fClearDirty [in ] BOOL fSaveAllProperties); HRESULT Is D ir ty O ; PERSISTENCE \H ATL IPropertyBag2 is aii enhancement of the IPropertyBaa interface. IP roperty- Bag2 allows the object to obtain the number of properties in the bag and the type information for each property through the use of the C ountProperti es and Get- P ropertylnfo methods. A property bag that implements I Property Bag 2 must also support IPropertyBag so that objects that only support IPropertyBag can access their properties. Likewise, an object that supports IP ersi stP roperty- Bag2 must, also support. IP ersi stPropertyBag so that it can communicate with property bags that only support IPropertyBag. When the object wants to read a property in IP ersi stPropertyBag2 :: Load, it w ill call IPropertyBag2:: Read. When the object is saving properties in I Per- si stPropertyBag2 : : Save, it will call IPropertyBag2: :W rite. The client re­ cords errors that occur during Read with the supplied IE rrorLog interface. Implementing IPersistPropertyBag Clients will only ask sn object to initialize itself once. When the client has no initial values to give the object, the client calls the object's IP ersi stPropertyBag : : In i tNew method. In this case, the object should initialize itself to default values. When the client has initial values to give the object, it will load the properties into a property bag and call the object’s IP e rsi stPropertyB ag:: Load method. When the client wishes to save the state of an object, it creates a property bag and calls the object’s IP ersi stPropertyBag:: Save method. This is pretty straightforward to implement in an object. For example, the Dem- agogue object has three properties: its name, speech, and volume. Here’s an ex­ ample of an implementation to save and restore these three properties from a property bag: cla ss ATL_NO_VTABLE CDemagogue : p u b lic IP e rs i stP ro p e rtyB ag , BEGIN^COM^MAP(COemagogue) COM_INTERFACE_ENTRY(IPersistPropertyBag) COM一INTERFACE一 ENTRY2(IPersist, IPersi stPropertyBag) END_COM_MAP() CComBSTR m_name; 1ong m_vo1ume; CComBSTR m_speech; STDMETHODIMP Load (IPropertyBag *pBag, TFrrorLog *pLog) ATL INTERNALS the VARIANT to VT^BSTR pLog); the VARIANT to VT_: VARIANT to VT3STR CComVariant v ((BSTR) NULL); // In itia lize HRESULT h r =» pBag->Read(OLESTR(nNamew) , &v, if (FAILED (hr)) return hr; m_name = v .b s trV a 飞; v = OL; // Initialize hr = pBag->Read(0LESTRC"Vo1urre"), &v, pLog); if (FAILED (hr)) return hr; m_volume = v . lV a l; v = (BSTR) NULL; / / I n i t i a l i z e the hr = pBag->Read(OLESTR("Speech"), &v, pLog); if (FAILED (hr)) return hr; m_speech = v .b s tr V a l; re tu rn S_OK; } STDMETHOOIMP Save (IP ropertyB ag *pBag, BOOL fClearDi rty, BOOL /★ fSaveAllProperties */) { CComVariant v = iruname; HRESULT h r = pBag->Wri te(OLESTR(,,NameM) , & v ); if (FAILED(hr)) return hr; v = oi-volume; hr = pBag->Write(OLESTR("Vo1ume"), &v); if (FAILED(hr)) return hr; v = m_speech; hr « pBag->Write(OLESTR("Speech”), & v ); if (FAILED(hr)) return hr; if (fClearDi rty) m_fDi rty = FALSE; re tu rn h r: The 丨Stream, IPersistStreamlnit, and IPersistStream Interfaces COM objects that wish to save their state efficiently as a binary stream of bytes typ­ ically implement IP ersi stStream or IP ersi stStream lni t. An ActiveX control that has persistent state must, as a minimum, implement either IP e rsi stStream PERSISTENCE IN ATL 263 or IP ersi stS tream lni t. The two interfaces are mutually exclusive and shouldn't be implemented together, generally speaking. A control implements IP ersi s t- Stream lnit when it wishes to know when it is newly created as opposed to created and reinitialized from an existing persistent state. The IP ersistS tream interface does not provide a means to inform the control that it is newly created. The existence of either interface indicates that the control can save and load its per­ sistent state into a stream, that is, an instance of I St ream The IStream interface closely models the Win32 file API, and you can easily implement the interface on any byte-oriented media. COM provides two implemen­ tations of IStream, one that maps to a file and another that maps to a memory buffer. interface IStream : IUnknown { HRESULT Read([out] void *pv, [in ] ULONC cb, [out] ULONC *pcbRead); HRESULT W rite([in] const void *pv, [in ] ULONC cb, [out] ULONC *pcb); HRESULT S eckC tin] LARCE^INTECER dlibM ove, [in ] DWORD dwOrigin, [o u t] ULARCE.INTECER *plibN ^w P os nt erf ace IPersi stStreamlni t •• public IPersi st { HRESULT IsD i r t y O ; HRESULT Load([1n] LPSTREAM pStw); HRESULT S a v e ([in ] LPSTREAM pStm. [ in ] BOOL fC le a r D irty ) ; HRESULT C€tSizeMax([out] ULARGE.INTECER*pCbSizc); HRESULT In itN e w O ; Interface IPersistStream : public IPersist { HRESULT Is D irty O ; HRESULT Load(C in] LPSTREAM pStm); HRESULT S a v e ([in ] LPSTREAM pStm, [ in ] BOOL fC le a rO irty ); HRESULT CetSizeMax([out] ULARCE^INTECER^pCbSize); When a client wants an object to save its state as an opaque stream of bytes, it typ­ ically attempts to use the object’s IP ersi stS tream lni t interface as the persis­ tence mechanism. The client supplies the stream into which the object saves in the form of an IStream interface. 264 ATL INTFRNALS When the client calls IPe rs i stS tream lni t :: Load, the object roads its prop­ erties from the stream by calling I St re am:: Read. When the client calls IP e r­ si stStream lni t : :Save, the object writes its properties by calling IS tream :: W rite. Note that unless the object goes to the extra effort of handling the situation, the stream contains values in an architecturc-spocific byte order. Most recent clients prefer to use ait object’s IP ersi stS tream lni t interface, and, if it’s not present, fall back and try to use IP e rsi stStream. However, older client code only attempt to use an object/s IP e rsi stStream implementation.1 To be compatible with both types of clients, your object needs to implement IP er­ si stStream. However there are other containers that ask only for IP ersi s t- St reamlni t ,so to be compatible with them, you need to implement that interface.2 But you’re not supposed to implement both interfaces, because then it’s unclear to the container whether the object needs to be informed when it’s newly created. Pragmatically, the best solution to this dilemma is to support both interfaces when your object doesn't care to be notified when it's newly created— even though this violates the specification for controls. Although IPersi stStream lni t doesn’t derive from IPersi stStream (it can't, because of the mutual exclusion aspect of the interfaces), they have identical vtables for all methods except the last— the In i tNew method. Due to COM’s binary compatibility, when your object doesn’t need an In i tNew call, your object can hand out an IP ersi stStream lni t interface when asked for an IPersistStream in­ terface. So with a single implementation of IP ersi stStream lni t and an extra COM interface map entry, your object becomes compatible with a larger number of clients. cla ss ATL_NO_VTABLE CDemagogue : p u b lic IP e rs is tS tre a m ln it, BEGIN_COM_MAP(CDemagogue) • ■秦 COM_INTERFACE,ENTRY(IPersistStreamlnit) C0M_INTERFACE_ENTRY2(IPersistStream, IPersi stStreamlni t) C0M^INTERFACE,ENTRY2(IPersist, IPersistStream lnit) END一 COM一MAP() 1 ATL 3.0’s implementation of CComVARIANT's ReadFromSt ream and Wri teToSt ream methods only at­ tempts to use IP e rs i stStream when asked to read/write a VARIANT containing ai\ interface pointer. The next version of ATL will also attempt to use IP e rs i stS tre a m ln i t. 2ATL 3.0'simplementation of CAxHostWindow: :ActivateAx only uses IP e rsi stStreamlni t. PERSISTENCE IN ATL 265 Implementing IPersistStreamlnit Clients will only ask an object to initialize itself once. When the client has no initial values to give the object, the client calls the objects IP e rsi stSteam lni t :: In i t - New method. In this case, the object should initialize itself to default values. When the client has initial values to give the object, the client w ill open the stream and call the object’s IP e rsi stS tream lni t :: Load method. When the client wishes to save the state of an object,it creates a stream and calls the object’s IP ersi s t­ Stream lni t: :Save method. Like the property bag implementation, this is pretty straightforward to imple­ ment in an object. Here’s an example of an implementation for the Demagogue ob­ ject to save and restore its three properties to and from a stream. Cl ass CDemagogue : public IPersistStream lnit, • • ■ CComBSTR m_name; long m_volume; CComBSTR m_speech; STDMETHODIMP IsD i r ty () { re tu rn m_fDi r ty ? S^OK : S_FALSE STDMETHODIMP Load (IS tream * pStream) { HRESULT h r = m_name.ReadFromStream(pStream); if (FAILED (hr)) return hr; ULONC cb; hr = pStream->Read (&tn_volume, sizeof (m_volume), &cb); if (FAILED (hr)) return hr; h r : m_speech.ReadFromStream(pStream); if (FAILED (hr)) return hr; m_fDirty * FALSE ; re tu rn S一OK; } STDMETHODIMP Save (IS tream * pStream) { HRESULT hr = iname.WriteToStream (pStream); if (FAILED(hr)) return hr; ULONC cb; hr «: pStream->Write(&m_volume, sizeof (m_volume) , &cb); if (FAILED(hr)) return hr; 266 ATL INTERNALS hr = m_speech.WriteToStream (pStream); re tu rn h r; } STDMETHODIMP GetSi zeMax(ULARCE_INTEGERApCbSize) { i f (NULL == pCbSize) re tu rn E_POINTER; pCbSize->QuadPart = sizeof (ULONC); / / length of m_name pCbSi ze->QuadPart += m一 name ? SysStringByteLen(m_nanie) + sizeof(OLECHAR) : 0; pCbSize->QuadPart +* sizeof (m_volume); pCbSize->QuadPart + - s iz e o f (ULONC); / / le n g th o f m_speech pCbSi ze->QuadPart +== m_speech ? SysStringByteLen(m_speech) + sizeof(OLECHAR) : 0; re tu rn S_OK; } STDMETHOOIMP In itN e w Q { re tu rn S_OK; } IStorage and IPersistStorage An embeddable object— an object that you can store in a OLE linking and embed­ ding container such as Microsoft Word and Microsoft Excel_ must implement IPersistStorage. The container provides the object an IStorage interface pointer. The IS torage interface pointer references a structured storage medium. The storage object (the object implementing the IStorage interface) acts much like a directory object in a traditional file system. An object can use the IS torage interface to create new and open existing substorages and streams within the stor­ age medium provided by the container. interface IStorage : public IUnknown { HRESULT C re a te S tre a m C [strin g , in ] const OLECHAR* pwcsName v [1n] OWORO grfMode, [in ] OWORO reservedl, [in ] 0M0R0 rcserved2, [out] IStreaw** ppstm); HRESULT O penStream C [string, in ] const OLECHAR* pwcsName, [unique][1n] void* reservedl, [in ] OWORO grfMode, [in ] DWORO reserved2, [out] IStream** ppstm); HRESULT CreateStorage( [s t ri ng,1n] const OLECHAR* pwcsNamc, [in ] DWORO grfMode, [in ] 0W0RD reservedl, [in ] DWORD reserved2t [out] IStorage** ppstg); HRESULT OpenStorage( [ s t r i ng, uni que, i n] const OLECHAR* pivcsName PERSISTENCE 丨 N ATL [unique,in] IStorage* pstgPriority, [in ] DWORD grfMode, [unique,in] SNB snbExclude [in ] DWORD reserved, [out] IStorage** ppstg); // Following methods abbreviated for c la rity .•• HRESULT CopyToC ...); HRESULT MoveElewentTo(. . . ) MRESULT Commit( . . . ) HRESULT R e v e rt(v o id ); HRESULT EnuwElementsC ...); HRESULT DestroyElementC ...); HRESULT RenameE1ement( ...); HRESULT S€tEle««ntT1ines( . . . )•• HRESULT SetClassC •..); HRESULT S € tS ta te B its( ...); MRESULT Stat(. . . ) ; Interface IPersistStorage : public IPersist { HRESULT Is D ir ty ( ) ; HRESULT InitN«w ([unique*in] IStorage* pStg); HRESULT Load ([unique,in] IStorage* pStg); HRESULT Save ([unique,in] IStorage* pStgSave, [ in ] 800L fSameAsLoad); HRESULT SaveCoiBpleted ([u n iq u e , 1n] IStorage* pStgNew) MRESULT HandsOffStorage () ; The IsD i rty , In i tNew, Load, and Save methods work much as the similarly named methods in the persistence interfaces you’ve seen previously. However, unlike streams, when a container hands an object an IStorage during the In i t - New or Load call, the object can hold onto the interface pointer (after AddRef'ing it, of course). This permits the object to read and write its state incrementally, rather than all at once as do the other persistence mechanisms. A container uses the HandsOffStorage and SaveCompleted methods to instruct the object to release the held interface and to give the object a new IStorage interface, respectively. Typically, a container of embedded objects w ill create a storage to hold the ob-rage to itreamsjects. In this storage, the container will create one or more streams to hold the container’s own state. In addition, the container, for each embedded object, creates a sub-storage in which the container asks the embedded object to save its state. ATL INTERNALS This is a pretty heavyweight persistence technique for many objects. Simple ob­ jects, like the Demagogue example used so far, don't really need this flexibility. Of­ ten, such objects simply create a stream in their given storage and save their state into the stream using IP ersi stS tream lni t. As this is exactly what the ATL im­ plementation of IP ersistS torage does, 111 defer creating a custom example here; you’ll see the ATL implementation shortly. ATL Persistence Implementation Classes ATL provides implementations of the IP ersi StPropertyBag, IP ersi stStream, and IPersistStorage interfaces called IPersistPropertyBaglmpl, IPer­ si stStreamlmpl,and IP ersi stStoragelm pl,respectively. Each template class takes one parameter, the name of the deriving class. You add support for these three persistence interfaces to your object like this: c la s s ATL_NO_VTABLE COemagogue : public IPersistPropertyBaglmpl, public IPersi stStreamlnitlmpl, public IPersi stStoragelmpl {... BEGIN_COM_MAP(CDemagogue) C0M_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag) COM_INTERFACE_ENTRY(IPersistPropertyBag) COM_INTERFACE_ENTRY(IPersistStreamlnit) COM_INTERFACE_ENTRY(IPersistStorage) END一 CGM_MAP() Don’t forget to add the COM MAP entry for IP e rs i st. All three persistence interfaces derive from IP ersi st, not IUnknown, so you need to respond affirmatively to queries for IP e rs i s t. Note also that you need to use the COM 一 INTERFACE一ENTRY2 (or COM_INTERFACE_ENTRY_IID) macro because there are multiple base classes derivmg from IP ersi st. The Property Map The ATL implementation of these three persistence interfaces requires that your ob­ ject provide a table describing all the properties that should be saved and loaded during a persistence operation. This table is called the property map. ATL uses the PERSISTENCE IN ATL 269 property map of a class for two independent purposes: persistence support and control property page support (discussed in Chapter 10). The various property map entries allow you to ■ Define the properties of the COM object that the ATL persistence implemen­ tation classes save and restore during a persistence request. ■ Define the member variables of the C+ + class that the ATL persistence imple­ mentation classes save and restore during a persistence request. ■ Define the property pages used by a class. ■ Associate a properly with its property page. The ("Demagogue class’s property map looks like this: BECIN.PROP.MAPCCDemagogue) PROP_ENTRY_EXCnSpeechM, DISPID_SPEECH, CLSID.NULL PROP_F.NTRY_EXC'Vol umeM , DISPID—VOLUME, tLSID.NULL PROP. ENTRY^EXC'Nane", DISPID_NAMEf CLSID.NULL END_PR0P_MAP() IID_ISpeake「) IlD_lbpeake「) IID_INamedObject) The BEGIN_PR0P_MAP and END_PROP_MAP macros define a class's property map. When you create an object with the ATL Object Wizard, the wizard w ill create an empty property map by specifying BEGIN一PR0P_MAP followed by END一 PROP—MAP. You list the persistent properties of an object in the property map using the PROP_ENTRY and PR0P_ENTRY_EX macros. The PROP_ENTRY macro describes a property that the persistence implementation can access via the default dispatch interface, or, in other words, the interface retrieved when you query for IID —ID i s- p a tch . You use the PR0P_ENTRY一EX macro to describe a property that the persis­ tence implementation must access using some other specified dispatch interface. Both macros require the name of the property, the property's DISPID, and the CLSID of the property's associated property page (discussed in Chapter 10). The PROP_ENTRY_EX macro also requires the IID of the dispatch interface that supports the specified property, whereas the PROP一ENTRY macro uses IID 一 IDi spatch: PROP一ENTRY (szDesc, dispid, clsid) PROP_ENTRY_EX (szDesc, dispid, clsid , i idDispatch) PROP_DATA_ENTRY (szDesc, -nember, v t ) You may also want to load and save member variables of your object that are not accessible via a dispatch interface. The PR0P_DATA_ENTRY macro also allows you 270 ATL INTERNALS to specify1 the name of a propert y, the member variable containing the value, and the VARIANT type of the variable. BtGIN_PROPJ^AP(CBullsEye) PRGP_DATA__ENTRY(” _ c x .., m_si z e E x te n t.c x , VT_U14) PROP_DMA^ENTRY(”_cy" • m_.si zeExtent. cy, VT.UI4) • • • EN0_PR0P_MAP() Effectively, the PROP一DATA_ENTRY macro causes the persistence implementation to reach into your object, access the specified member variable for the length im­ plied by the VARIANT type, place the data into a VARIANT, and write the VARIANT to the persistent medium. This is quite hai\dy when you have a member variable that is a VARIANT-compatible type. However, it doesn’t work for noncompatible types such as indexed properties. Note: There is also a PRO PIPAGE macro used to asso­ ciate a property to a property page. I discuss its use in Chapter 10. The persistence implementations skip ent ries in the property map made with the PRO PIPAGE macro. One caution: Don’t add a PROP_ENTRY, PROP_ENTRY_EX, or PR0P_DATA_ ENTRY macro tbat has a property name containing an embedded space character. Some relatively popular containers, for example Visual Basic, provide an imple- m^T\tation of IPropertyBag that cannot handle names with embedded spaces This is a bug in the current version of Visual Basic, as other containers allow space- separated property names When you have a member variable you want to load and save during a persis­ tence operation and Uiar variable is not, a VARIANT-compatible type (for example, an indexed or array variable), the property raap mechanism doesn’t help. You have to override the appropiiate member functions of tho persistence implementation classes and read and wnte the variable explicitly. To do this, you need to know the basic structure of the ATL persistence implementation. The Persistence Implementations Lot's look at how the persistence implementations work. As you’ll see shortly, the ATL 3.0 property bag persistence implementation has a bug Fd like to fix, so I'll use ;,t as the example. However, all persistence implementations are similar. The Property Map The property inap macros basically add a static member function called GetProp- ertyMap to your class. CetPropertyMap returns a pointer to an array of ATL_ PROPMAP.ENTRY structures. This structure looks like this: PERSISTENCE IN ATL 271 S tru c t ATL_PROPMAP_ENTRY 1 LPCOLESTR szDesc; DISPID dispid; const CLSID* pclsidPropPage; const IID* pi 1 (IDispatch; OWORD dwiOffsetOata; DWORD dwSizeData; VARTYPE v t; For example, here’s a property map and the resulting macro expansion. The prop­ erty map BECIN_PROP_MAPCCDetnagogue) PROP_ENTRY ("Speech", PROP—ENTRY—EX ( HName", PROP一DATA^ENTRY("-CX", END^PROP_MAP() DISPID.SPEECH, CLSIO_NULL) DISPID—NAME, CLSID_NULL, IID^INamedObject) m_sizeExtent.cx, VT_UI4) expands to this: typ e d e f J\TL_PROPJiOTIFY__EVENT—CLASS 一 ATL—PROP_NOTIFY_EVENT_CLASS; typedef COemagogue _PropMapC1ass; s ta tic ATL_PROPMAP_ENTRY^ GetPropertyMapO { s ta t ic ATL^PROPMAP.ENTRY pPropMap[] = { {OLESTR(("Speech"), DISPID—SPEECH, &CLSID—NULL, 0, 0, 0}, {OLESTR("Name”), DISPID_ NAME, &CLSID—NULL, 0, 0, 0}, {0LESTR(”_cx”), 0, &CLSID_NULL, offsetofC.PropMapClass, m_sizeExtent.cx), sizeof((C_PropMapClass*)0)-> m_sizeExtent.cx), VT—UI4}, {NULL, 0. NULL, &IID^NULL, 0, 0, 0 } } } The szDesc field of the structure holds the name of the property. It’s only used by the property bag persistence implementation. The other common persistence mechanisms don’t require a textual name for a property. The d i spi d field contains the property’s dispatch identifier. All the persistence implementations need this so they can access the property via one of the object's ID i spatch implementations by calling the Invoke method. &IID_IDispatch, &IID—INamedObject, NULL, 272 ATL INTERNALS The pci si dPropPage field contains a pointer to the CLSID for the property page associated with the object. It’s not used during persistence. The pi i dDi spatch field contains a pointer to the IID of the dispatch interface that supports this property. The specified di spi d is unique to this interface. The last three fields are used only by PROP_DATA_ENTRY macros. The dw O ff- setData field contains the offset of the specified member variable from tJhe begin­ ning of a class instance. The dwSi zeData field contains the size of the variable in bytes, and the v t field contains the variable’s VARTYPE (VARIANT type enumeration code). The various persistence implementations basically iterate over this map and load or save the properties listed. For properties listed using PROP_ENTRY and PROP_ENTRY_EX, the implementations call ID is p a tc h :: Invoke with the speci­ fied di spid to get or put the property. Invoke transfers all properties via a VARIANT. The stream persistence imple­ mentation simply wraps the variant in a CComVARIANT instance and uses its Read- FromStream and WriteToStream methods to do all the hard work. Therefore stream persistence supports all VARIANT types supported by the CComVARIANT persistence implementation (discussed in Chapter 2). The properly bag implemen­ tation has it even easier because property bags deal directly in VARIANTS. For properties listed using the PROP一DATA^ENTRY macro, things aren’t quite so simple. The IP ersi stS tream lni t implementation directly accesses the object in­ stance at the specified offset for the specified length. The implementation reads or writes the specified number of bytes directly to or from the object. The VARIANT type is completed ignored. However, the IP ersi stPropertyBag implementation must read and write properties held in a VARIANT. Therefore, this implementation copies the member variable of the object to a VARIANT before writing the property to the bag and copies a VARIANT to the member variable after reading the property from the bag. The current implementation of IP ersi stPropertyBag persistence only supports a limited set of VARIANT types; what’s worse is that it silently fails to load and save properties with any VARTYPEs other than these: V T_U I1, VT_I1 Reads and writes the variable as a BYTE. VT_B00L Reads and w rite the variable as a VARIANT_BOOL. VT_L)I2 Reads and writes the variable as a short. VT_UI4, VT_INT, Reads and writes the variable as a long. VT UINT Strangely, commonly used VARIANT types, such as VT一12 (signed short) and VT_I4 (signed 1 ong) aren't supported, though it's simply a matter of adding a few PCRSiSTEMCE I N ATL 273 m on1 Cdb U» \iui “ >‘k ' i txpoci this to l>e c*oriectc(J in the next ATL release IPersistPropertyBaglmpI The IP e^s,t P r ope r I y Bag I inp I M< • >; :«、. l lUSSi'S template class ATL—NO—VTABLE IPersistPropertyBaglmpI : public IPersi StPropertyBag { p u b lic : STDMETHOOCCetClassID)(CLSID ^pCIassID) { All rRACE2(atlTraceC0M, 0, 一T("IPersistPropertyBaglmpl: ;CetClassID\n")); *pClassID - T::CetObjectCLSID(); re tu rn S一OK: Normal, >ur rlctvs obtains ns GetObjectCLSID static member function from CComCo( i -iss. template cla ss CComCoClass { p u b lic : static const CLSID& WINAPI GetObjectCLSIDO {return *pclsid;} 274 ATL INTERNALS This implies that a class must be createable in order for it to use the persistence classes. This is reasonable because it doesn’t do much good to save a class to some persistent medium and then be unable to create a new instance when loading the object from that medium. The IP ersi stPropertyBaglmpl class also implements the remaining IP e rsi stPropertyBag methods, including, for example, the Load method. IP er­ si stPropertyBaglmpl::Load call T ::IPersistPropertyBag 一Load to do most of the work. This allows your class to provide this method when it needs a custom implementation of Load. Normally, your object (c l ass T) doesn’t pro­ vide an IPersi stPropertyBag 一 Load method, so this call vectors to a default implementation provided by the base-class IP ersi stPropertyBaglm pl:: IPersi stPropertyBag 一 Load method. The default implementation calls the global function A tl IP ersi stPropertyBag 一 Load. This global function iterates over the property map and, for each entry in the map, loads the property from the property bag. tempi ate i class ATL«NO«VTABLE IPersistPropertyBaglmpl : public IPersi stPropertyBag { p u b lic : » « • / / IPersistPropertyBag // STOMETHOO(Load)(LPPROPERTYBAC pPropBag, LPERRORLOC pErrorLog) { ATLTRACE2(atITraceCOM, 0, _T("IPersistPropertyBaglmpl: :Load\n")); T* pT • static_cast(this); ATL^PROPMAP^ENTRY* pMap - T ::C e tP ro p e rtyM a p (); ATLASSERT(pMap ! • NULL): return pT->IPersistPropertyBag_Load(pProp8ag, pErrorLog, pMap); } HRESULT IPersistPropertyBag^Load( LPPROPERTYBAC pProp6ag, LPERRORLOC pErrorLog, ATL_PROPMAP_ENTRY* pMap) T* pT « static_cast(this); HRESULT hr ■ AtlIPersistPropertyBag_Load(pPropBag, pErrorLog, pMap, pT, pT->C€tUnknown()); :i i f (SUCCEEDED(hr)) pT->m_bRequiresSave - FALSE; PERSISTENCE IN ATL 275 re tu rn hr > This implementation structure provides three places where we can override meth­ ods ai\d provide custom persistence support for a non-VARIANT-compatible prop­ erty. We could override the Load (LPPROPERTYBAC pPropBag, LPERRORLOG pErrorLog) method itself, in effect directly implementing the IP ersi stProper­ tyBag method. Alternatively, we can let ATL implement Load while our object im­ plements IP ersi stPropertyBag_Load. Finally, we can let ATL implement Load and IPersi stPropertyBag 一 Load while we provide a replacement global func­ tion called A tl IP ersi stPropertyBag_Load and play some linker tricks so our object uses our global function rather than the ATL-provided one. The most natural method is to implement Load. Normally, in this implementation y o u ,d call the base-class Load method to read all properties described in the prop­ erty map, then read any custom, non-VARIANT-compatible properties. For example: HRESULT CMyObject:: Load(LPPROPERTYBAC pPropBag, LPERRORLOG pErrorLog) { HRESULT hr - IPersistPropertyBagImpl::Load(pPropBag, pErrorLog); If (FAILED (hr)) return hr; II Read an a rra y o f VT—I4 II This requires us to create a ••name" for each array element / / Read each element as a VARIANT, then recreate the array This approach has a few disadvantages. It’s a minor point, but the object now re­ quires four methods for its persistence implementation— its Load, the base-class Load, the base-class IPersistPropertyBag_Load, and A tllP ersistP roper- tyBag 一 Load. I could copy the base-class Load implementation into the object's Load method, but that makes the object more fragile because future versions of ATL might change its persistence implementation technique. Another slight disadvantage to this approach is that it is clear from the ATL im­ plementation that the ATL designers intended for your object to override IP e r­ si stPropertyBag^Load. Note the following code fragment from the default implementation of Load: 276 ATL INTERNALS STDMETHOD(Load)(LPPROPERTY8AC pPropBag, LPERRORLOC pErrorLog) { • • • pT * static«cast(this); • • • return pT->IPersistPropertyBag_Load(pPropBag, pErrorLog, pMap); } Rather than directly calling IP ersi stPropertyBag 一 Load, which is present in the same class as the Load method, the code calls the method using a pointer to the de­ riving class_ your object’s class. This provides the same functionality as making the method virtual without the overhead of a virtual function. A more significant disadvantage is.that calling the base-class Load method in tum calls the base class IP ersi stPropertyBaglmpl:: IP ersi stProper- tyBag_Load method. That function in tum calls the A tl IP ersi stP roperty- Bag一 Load function, which has a bug. The ATL 3.0 implementation of A tH - Persi stPropertyBag_Load for properties described using PROP_DATA_ENTRY macro is slightly flawed. Here’s 狂 code fragment from that function: CComVariant var; if (pMap[i].dwSizeData !• 0) { void* pOata * (void*) (pMap[i].dwOffsetOata + (OWORO)pThis); var.vt = pMap[i].vt; // BUG FIX line added HRESULT hr = pPropBag->Read(pMap[i] . szDesc, Avar, pErrorLog); if'(SUCCEEDEO(hr)) { switch CpMap[1}.vt) { case VT_UI4: *((long*)pData) » var.IVal; break; } } The CComVari ant constructor initializes var to VT一 EMPTY. An empty input VARI­ ANT permits the IP ropertyB ag:: Read method to coerce the value read to any ap­ propriate type. Note, however, that the code copies the VARIANTS value into the member variable of the control based on the type specified in the property map en­ try, regardless of the type contained in the VARIANT. When the _cx and _cy extents (described in a property m^> using PROP一 DATA^ENTRY macros previously) are small enough, the Read method initializes the variant to contain a VT_I2 (short) value. However, the property map entry PERSISTENCE IN ATL 277 specifies that the member variable is VT_UI4 type. In other words, the data source is 16 bits, the destination is 32 bits, and the code copies 32 bits. In this case, this code sets the high-order 16 bits of the controls extents to bogus values. Initializing the VARIANT type to the type contained in the property map, as shown in the “BUG FIX line added” statement in the code fragment, fixes the problem. I could play some linker tricks and provide a custom A tl IP ersi stP ro p e rty­ Bag function that gets linked in preference to the one ATL provides. This approach fixes the problem for all classes that use the property bag persistence implementa­ tion. However, it’s easier (and less fragile) to have the object provide a n lP e rs is t- P roper ty Bag一Load method that simply calls a fixed version of A tl IP ersi s t- PropertyBag 一 Load (Of course, you can always simply go correct the header file, but that leads to its own set of maintenance issues.) It seems the best solution is to let the object provide its own implementation of the IP ersi stPropertyBag_Load method. In this implementation, the object can call a corrected version of A tl IP ersi stProperyBag 一Load and save any non- VARI ANT-compatible properties it possesses. Here’s an example from the BullsEye control described in Chapter 10. It contains a property that is an array of long in­ tegers. This can’t be described as a VARIANT-compatible type as it’s not a SAFE- ARRAY, so I can’t list the property in the property map. HRESULT CBul^sEye: : IPersistPropertyBag_Load(LPPROPERTYBAC pPropBag, LPERRORLOG p E rro rL o g , ATL_PROPMAP_ENTRY* pMap) { i f (NULL *= pPropBag) re tu rn E.POINTER; .// Load the properties described in the PR0P_MAP // Work around ATL 3.0 bug in AtlIPersistPropertyBag_Load HRESULT h r = Fi xedAtlIPersi stPropertyBag_Load(pPropBag, pErrorLog, pMap, thi s, CetUnknownO); i f (SUCCEEDED(hr)) m_bRequi resSave = FALSE; if (FAILED (hr)) return hr; // Load the indexed property-RingValues // Get the number of rings s h o rt sRi ngCount; get_RingCount (&sRingCount); II For each ring, read its value for (short nlndex = 1; nlndex <= sRingCount; nlndex++) { 278 ATL INTERNALS // Create the base property name CComBSTR bstrName = OLESTR("RingValue”); // Create ring number as a string CComVariant vRingNumber = nlndex; hr = vRingNumber.ChangeType (VT_BSTR); ATLASSERT (SUCCEEDED ( h r ) ) ; // Concatenate the two strings to form property name bstrName += vRingNumber.bstrVal; // Head ring value from the property bag CComVariant vValue = OL; hr = pPropBag->Read(bstrName, AvValue, pErrorLog); ATLASSERT (SUCCEEDED ( h r ) ) ; ATLASSERT (VT—I4 == v V a lu e .v t); if (FAILED (hr)) { h r = E.UNEXPECTED; break; } II Set the ring value put_RingValue (nlndex, vValue.TVal); } i f CSUCCEEDED(hr)) m—bRequiresSave = FALSE; re tu rn h r; } The Save method works symmetrically. The IP e rsi stPropertyBaglm pl class implements the Save method. IP e rsi stPropertyBaglm pl :: Save calls T:: IP ersi stPropertyBag_Save to do the work. Again, your object (cl ass T) doesn’t normally provide an IPersistPropertyBag_Save method, so this call vectors to a default implementation provided by the base-class IP e rsi stP roper­ tyBaglmpl : :IPersi stPropertyBag 一 Save method. The default implemen­ tation calls the global function A tl IP ersi stPropertyBag 一 Save. This global function iterates over the property map and, for each entry in the map, saves the property to the property bag. PERSISTENCE IN ATL 279 template class ATL_NO.VTABL£ IPersiStPropertyBaglmpl : public IPersi stPropertyBag { p u b lic : 參眷 《 // IPersi stPropertyBag // STOMETHOO(Save) ( LPPROPERTYBAC pPropBag, BOOL fC le a rO irty . BOOL fSaveAllProperties) { ATLTRAC62(atlTraceCOM, 0, .TCIPersistPropertyBaglmpl: :Save\n°)); T* pT ■ static_cast(this); ATL- PROPMAP_ENTRY# pMap • T : :C etP ropertyM ap(); ATLASSERT(pHap !* NULL); return pT->IPersistProperty8ag_Save(pPropBag, fClearO irty, fSaveAl1Properties’ pHap); } HRESULT IPersistPropertyBag^SaveCLPPROPERTYBAC pPropBag, BOOL fC learO irty, BOOL fS a v e A llP ro p e rtie s , ATL.PROPMAP^ENTRY* pMap) { T* pT ■ s t a t ic 一cast(this); return AtlIPersistPropertyBag_Save(pProp8agf fClearOirty, fS aveA l1Properties, pMap, pT, pT->CetUnknown()); Finally, IPersi stPropertyBaglmpl implements the In i tNew method this way: STDMETHOD(InitNcw)() /1 ATLTRACE2(atUraceC0M.()• _T("IPersistPropertyBagImpl: ilnitNewXn**)); re tu rn S_0K; } 擊 Therefore, you’ll need to override In i tNew directly when you have any initializa­ tion to perform when there are no properties to load. 280 ATL INTERNALS IPerslstStreamlnitlmpI The iinplementation contained in IP ersi stStream lni tlm pl is quite similar to the one just described. The Load and Save methods call the IP e rsi stStream ­ ln i t_Load and IP ersi stStream lni t_Save methods, respectively, potentially provided by the deriving object but typically provided by the default imple­ mentation in IP ersi stStream lni tlm p l. These implementations call the global helper functions A tlIP ersi stStreamlni t_Load and A tlIP ersi stStream­ ln i t_Save. template c la s s ATL_N0„VTABLE IP e rs is tS tre a m ln itlm p l : public IPersi stStreamlni t { p u b lic : • 秦鲁 // IPersistStream STOMETHOO(Load)(LPSTREAM pStm) { ATLTRACE2(atlTraceCOM t 0, .T("IP ersi stStreamlni tlm p l: :Load\n")); T* pT * static«cast(this); return pT->IPersistStreamlnit_toad(pStra, T: :CetPropertyMap()); > HRESULT IP ersi stStream lni t^toad(UPSTREAM pStm, ATL_PROPMAP—ENTRY* pMap) { T* pT * stat1c_cast(this); HRESULT hr _ A tlIP ersi stStreamlni t_Load(pStm, pMap. pT, pT->CetUnknown()); i f (SUCCEEOED(hr)) pT->m_bRequiresSave * FALSE; re tu rn h r; 齡 | 裏 ■ 麵 g , ST0METH00(Save)(LPSTREAM pStm. BOOL fC le a rD ir^ y ) { T* pT ■ static«cast(this); ATLTRACE2 (at 1TraceCOM, 0, .T (MIPersi stStreamlni tlm p l: :Save\n*')); return pT->IPcrsistStreamlnit.Save(pSt«, fClearDirty, T ::GetPropertyMapO); HRESULT IPersistStreamlnit«Save(LPSTREAM pStm, BOOL fC learD irty, ATL.PROPHAP.ENTRY* pHap) { PERSISTENCE IN ATL T* pT « static«cast(this); return AtlIPersistStreamlnit.Save(pStm, fClearDirty, pMap, pT• pT->Cetl)nknown()); IP ersi stStream lni tlm pl also implements the In i tNew method this way: STOMETHOO(InitNew)() { ATLTRACE2(atITraceCOM, 0, „T (MIPersistStream Initlm pl::InitN ew \n")); re tu rn S_OK; p 麥 ' v 上 拿 0 ' 广 3 " ; C r S l r 七 - Therefore, as with property bags, you’ll need to override InitN ew directly when you have any initialization to perform when there are no properties to load. The implementation of the IsD i rty method assumes the presence of a mem­ ber variable called m_bRequi resSave somewhere in your class hierarchy. ST0METH00(IsDirty)() { ATLTRACE2(atlTraceCOM, 0, _T(MIPersi stStreamlni tlm p l: :IsDi rty \n *')); T* pT ■ s ta tic _ c a s t< T * > (th is ); re tu rn CpT->m_bRequi resSave) ? S_OK : S^FALSE; } The persistence implementations originally assumed that they would only be used by ActiveX controls一 as if controls were the only objects that needed a persistence implementation. While the coupling between the control classes and the persis­ tence implementation is greatly reduced in ATL 3.0, it is the CComControlBase class that normally provides the m_bRequi resSave variable and the SetDi rty and CetDi rty helper ftinctions usually used to access the variable: cla ss ATL.NO.VTABLE CComControlBase { p u b lic : void SetDirty(BOOL bOirty) { m_bRequiresSave ■ bOirty; } // Obtain the dirty state for the control BOOL CctDirtyC) { return m_bRequiresSave ? TRUE : FALSE; } unsigned nubRequiresSave:1; 282 ATL INTERNALS To use the persistence implementation classes in an object that doesn’t derive from CComCont ro l Base, you’ll need to define the m一 bRequi resSave variable in your class hierarchy somewhere. Typically, for convenience, you’ll also define the S et- Di rty and CetDi r ty helper methods as well. So here’s a class that noncontrols can use to provide this persistence support: c la ss ATL_NO_VTABLE CSupportDi r ty B it { p u b lic : CSupportDi r t y B it () : m_bRequi resSave(FALSE) { } void SetDirty(BOOL bDirty) { m_bRequiresSave = bDirty ? TRUE : FALSE;} BOOL CetDirty() { return m_bRequiresSave ? TRUE : FALSE; } BOOL m_bRequiresSave; Finally, the IP ersi stStream lni tlm pl class provides the following imple­ mentation of GetSi zeMax: STDMETHOO(CetSi zeMax)(ULARCE^INTECER FAR* 卜 pcbSize */) { ATLTRACENOTIMPLCTC'IPersistStreamlnitlmpl: :CetSizeMax,,» ; } One could argue that this is an invalid implementation of GetSi zeMax. The Plat­ form SDK even warns that GetSi zeMax should always return a value equal to or greater than the number of bytes in the stream that the object requires in order to save its state. This is due to the fact that a caller may allocate a nongrowable stream based on the size returned by GetSi zeMax. Regardless, the lack of support for this method causes it to be difficult, though not impossible, to use the stream persis­ tence implementation provided by ATL when adding marshai-by-value support to the object. Youll see how to do that at the end of this chapter. IPers 丨财Storagelmpl The ATL implementation of IPersi stStorage is very simplistic. The Save method creates a stream called “Contents” within the provided storage and depends on an IP ersi stS tream lni t implementation to write the contents o( the stream. template class ATL_NO_VTABLE IPersistStoragelmpI : public IPersistStorage { PERSISTENCE IN ATL 283 p u b lic : STDMETHOO(Save)(IStorage* pStorage, BOOL fSameAsLoad) { ATLTRACE2(atlTraceC0M, 0. _T(MIPersistStorageIm pl: :Save\n**)); CComPtr p; p.p » IPSI.CetlPersistStreamlnit(); HRESULT h r • E.FAIL; i f (p ! • NULL) { CComPtr spStream; sta tic LPCOLESTR vszContents ■ OLESTR("Contents"); hr « pStorage->CreatcStream(vszContents, STOLREADWRITE 丨 STGM__SHARE-EXCLUSIVE 丨 STGM_XREATE, 0, 0, AspStream); •if (SUCCEEDEO(hr)) h r ■ p»>Save(spStream, fSameAsLoad); > re tu rn h r; Similarly, the Load method opens the “Contents” stream and uses the IP e r­ si stS tream lni t implementation to read the contents of the stream. STDHETHOO(Load)(IStorage* pStorage) { ATLTRACE2(atlTraceC0M, 0, •T("IPersistStorageIm pl: :Load\n")); CComPtr p; p.p • IPSI_CetIPersistStreamlnit(); HRESULT h r • E_FAIL; i f (p ! - NULL) { CComPtr spStream; hr _ pStorage- nStream(OLESTR("Cont«nts"), NULL, STOCOIREa CM_SHARE«EXCLUSIVE. 0, &spStream); i f (SUCCEEOEO(hr)) h r - p.>Load(spStream ); > re tu rn h r; } The In i tNew and IsD i rty implementations retrieve the object’s IP ersi s t- St ream lni t interface pointer (using a helper function to get the interface) and del­ egate to the same named method in that interface. 284 ATL INTERNALS STOMETHODCIsDirty)(void) { ATLTRACE2(at 1 TraceCOM, 0, J T ("IP e rs is tS to ra g e lm p l: :Is 0 i r t y \ n '* ) ) ; CComPtr p; p.p & IPSI_CetIPersistStreamInit(); return (p !* NULL) ? p->IsDirty() : E^FAIL; } STDMETHOO(InitNew)(IStorage*) { ATLTRAC£2(at1TraceC0M, 0, _T("IPersistStoragelm pl: :InitNew\n">); CComPtr p; p.p * IPSI.CetlPersistStreamlnit(); return (p !* NULL) ? p->InitNew() : E_FAIL; One of the main reasons an object supports IP e rsi stS torage is so the object can incrementally read and write its state. Unfortunately, the ATL implementation doesn’t support this. The implementation does not cache the IS torage interface provided during the Load and Save calls, so it’s not available for later incremental reads and writes. Not caching the IS torage interface makes implementing the last two methods trivial, however. ST0METH00(SaveComp1eted)(IStorage* /* pStorage */) { ATLTRACE2(atlTraceCOM, 0. _T("IPersi stStoragelmpl: : SaveCompleted\nM>); re tu rn S«0K; } STOMETHOO(HandsOffStorage)(void) { ATLTRACE2(atlTraceC0M, 0. _TC'*IPersi stStoragelmpl: :HandsOffStorage\n")); re tu rn S_0K; Generally speaking, most objects that need the functionality provided by IP e rsi stS torage won’t be able to use the implementation provided by ATL. They w ill have to derive directly from IPe rs i stS torage and implement all the methods explicitly. PERSISTENCE IN ATL 285 Additional Persistence Implementations Given what has been discussed so far, I thought it might be interesting to demon­ strate an additional persistence implementation built using functionality you’ve al­ ready seen. IPersistMemory Let’s look at implementing the IP e rsi stMemory interface. It looks like this: interface IPersistMemory : IPersist { HRESULT I s D ir t y O ; HRESULT L o a d ([in , s iz e .is (c b S iz e )] LPVOIO pvMem, [in ] ULONC cbSize); HRESULT S a v e ([o u tf s iz e _ is (c b S iz e )] LPVOID pvMem, [in ] BOOL fClearDirty, [in ] ULONC cbSize); HRESULT CetSizeMax([out] ULONC* pCbSize); HRESULT In itN e w O ; The IP ersi stMemory interface allows a client to request that the object save its state to a fixed-size memory block (identified with a voi d*). The interface is very similar to IP ersi stS tream lni t, except that it uses a memory block rather than an expandable IStream. The cbSize argument to the Load and Save methods indicates the amount of memory accessible through pvMem. The IsD i rty , Get­ Si zeMax, and In i tNew methods are semantically identical to those in IPe rs i s t- Streamlnit. Strangely, the header files show that the IP ersi stStream lni t : :CetSize- Max method expects a parameter that is a ULARGE_INTECER* and the I P e r s is t ­ Memory: :GetSi zeMax method expects a parameter that is a ULONG*. However, the documentation claims both methods are syntactically identical and expect a ULARGE—INTEGER*. Implementing the IPersistMemory Interface You’ve seen that ATL provides the IP e rsi stS tream lni tlm p l class that saves and restores the state of an object to a stream. The COM API CreateStreamOnH- Cl obal returns an IStream implementation that reads and writes to a global mem­ ory block. I can use the two together and easily implement IP e rs i stMemory using the functionality provided by IP ersi stStream lni tlm p l. ATL INTERNALS With the exception of the Load and Save methods, all methods in my IP e r­ si stMemory implementation simply delegate to the same named method in ATL’s IP ersi stStream lni t implementation. template cla ss ATL一NCLVTABLE IPersi stMemorylmpl : public IPersi stMemory { p u b li c : // IPersi st STDMETHODIMP GetCIassID(CLSID *pClassID) { ATLTRACE2(atlTraceCOM, 0 , _T(MIPersistMemorylmpl: :GetClassID\n")); T* pT *= static_cast(this); S,v psi = stati c一cast (pT); return psi->CetClassID(pC1assID); } // IPersi stMemory STDMETHODIMP IsD i r ty O { ATLTRACE2(atlTraceCOM, 0, _T("IPersistMemorylmpl::IsDirty\n")) T* pT = static一cast(this); S* psi - s t a t ic 一cast (p T ); return psi->IsDi r*ty(); . STDMETHODIMP Load(void* pvMem, ULONG cbSize) { ATLTRACE2(atlTraceC0M, 0, ^T(MIPersistMemorylmpl::Load\nH ) ) ; T* pT = stati c_cast(thi s); II Get memory handle HCLOBAL h = C lo b a lA llo c (GMEM_MOVEABLE, cb S ize ); i f (NULL == h) re tu rn E_OUTOFMEMORY; LPVOID pv = ClobalLock (h〕; E^OUTOFMEMORV; // Copy to memory block CopyMemory (pv, pvMem, cbSize); CComPtr spStrm; / / C reate stream on memory HRESULT hr = CreateStreamOnHCIobal (h, TRUE, AspStrm); if (FAILED (hr)) { PERSISTENCE IN AT L ClobalUnlock (h); Global Free (h ); r e tu rn h r; } / / Stream now owns the memory / / Load from stream S* psi = s t a t ic 一cast (pT); hr = psi->Load (spStrm); ClobalUnlock (h); r e tu rn h r; } SIDMETHODIMP S a v e (v o id * pvMem, BOOL fC le a rO i r t y , ULONC c b S ize ) { ATLTRACE2(atlTraceCOM, 0, _T("IPersistMemoryImpl: :Save\n")); T* pT = s t a t ic _ c a s t < T * > ( t h is ) : II Get memory handle HGLOBAL h = G loba l A llo c (GMEM_M0VEABLE, c b S iz e ); i f (NULL == h) re tu rn E_OUI0卜MEMORY; / / C reate stream on memory CComPtr spStrm; HRESULT hr = CreateStreamOnHClobal (h, TRUE, &spStrm); if (TAILED (hr)) { Global Free (h ); re tu rn h r; } / / Stream now owns the memory II Set logical size of stream to physical size of memory II (Global memory block allocation rounding causes differences) ULARGE.INTECER u l i ; uli.QuadPart = cbSize ; spStrm->SetSize (u li); S* psi = s ta tic _ c a s t (p T ); hr = psi->Save (spStrm, fClearDi rty ); if (FAILED (hr)) return hr; LPVOID pv = Global Lock (h ); i f (\ pv) re tu rn E_OUTOFMEMORY; 288 AT L INTERNALS / / Copy to memory block CopyMemory (pvMem, pv, c b S iz e ); re tu rn h r; } STDMETHODIMP CetSizeMax(ULONC* pCbSize) { i f (NULL == pCbSize) re tu rn E.POINTER; * pCbSize = 0 ; T* pT = static_cast(this); S* psi = static—cast (pT); ULARGE_INTEGER u li ; uli.QuadPart = 0; HRESULT hr = psi->GetSizeMax (& u li); if (SUCCEEDED (hr)) *pCbSize = u li.LowPart; re tu rn h r; } STDMETHODIMP In itN e w O { ATLTRACE2(atlTraceCOM, 0, _T("IPersi stMemorylmpl: : In i tNew\n")); T* pT = static_cast(this); S* psi »= stati c_cast (pT); return psi->InitNew(); // When/if Visual C++ supports partial specialization, this I I implementation would allow IPersi stMemorylmpl to use an // IPersistStream implementation. # i f 0 template STDMETHODIMP IP ersi stMemorylmpl :: InitNewO { ATLTRACE2(atlTraceCOM, 0, _T("IPe「sistMemorylmpl : :InitN ew \n')); re tu rn S_OK; } # e n d if Notice the use of s ta ti c_cast to down-cast the th is pointer to the deriving class, then up-cast the resulting pointer to an IP e rsi stS tream lni t*. I do this so I get a compile-time error when the class deriving from IP ersi stMemorylmpl doesn’t also derive from IP ersi stS tream lni t. This approach does require that the de­ PERSISTENCE IN ATL riving class not implement IP ersi stS tream lni t in an “unusual” way, such as on a tear-off interlace or via aggregation. Alternatively, I could have retrieved the IP ersi stS tream lni t interface using Q uerylnterf ace something like this: T* pT = static 一 cast(this); CComQIPtr psi 这 pT->CetUnknown(); However, then I might find out at runtime that there’s no IP ersi stStream lni t implementation available, which means my object then ends up saying it imple­ ments IP e rsi st Memory without the ability to do so. I prefer compile-time errors whenever possible, so I chose the former approach, accepting its limitations. Using the IPersistMemorylmpI Template Class An object uses this IP e rsi s t Memory implementation this way: cla ss ATL_NO_VTABLE 〔Demagogue : public IPersistStreamlnitlmpl2, public IPersistMemorylmpl, public CSupportDi rtyB it { BEGIN_COM-MAP(〔Demagogue) C0M_INTERhACE_ENTRY2(IPersist, IPersistStream lnit) COM_INTERFACE_ENTRV(IPersistStreamlnit) COM_INTERFACE_ENTRY(IPersistMemory) END_COM_MAP() The GetSizeMax Method With the exception of the IP ersi stStream [In i t ] :: GetSizeMax methods and the IP e rsi stMemory: : GetSizeMax method, all methods of each persistence in­ terface must be fully implemented. And, in fact, ATL’s implementation of IP er­ si stStream lni tlm pl : .-GetSizeMax takes advantage of this exception and simply returns E_NOTIMPL. However, as you’ll see shortly, it would be more useful if Get Si zeMax actually worked. So let's fix it— at least in common cases. I could override C etS i zeMax in my CDemagogue class, but then I’d have to do the same in every class that wants a working version of the method. A better approach is to define a new template class that derives from IP ersi stS tream lni tlm pl and 290 ATL INTERNALS adds an implementation of GetSi zeMax. Here’s one (partial) implementation. (For the complete listing, see the u t ilit y .h source file in the ATL Internals project.) template class ATL_N0_VTABLE IPersistStrearrlnitlm pl2 : public IPersi stStreamlni tlmpl { p u b lic : STDMETHOD(CetSizeMax)(ULARGE—INTEGER* pcbSize) // For each persistent property entry in the property map for (in t i * 0; pMap[i] . pci si dPropPage != NULL; i++) { i f (pMap[i].szDesc == NULL) c o n tin u e ; II Just use the actual size of a raw data entry if CpMap[i].dwSizeData != 0) { pcbSi ze->QuadPart += pMap[i] . dwSi zeData; c o n tin u e ; } // Fetch the new IDispatch interface when we don't already II have i t • • • // Fetch the property described in the property map • • • // Interface pointers persist as a CLSID // followed by the object's persistent stream switch (var.vt) { case VT_UNKN0WN: case VT^DISPATCH: { // Scalar types persist as their size case VT_UI1: case VT—II: pcbSi ze->QuadPart += sizeof(BYTE); c o n tin u e ; PERSISTENCE IN ATL return S_OK; This code is highly dependent on ATLs implementation of IP ersi stS tream lni t and CComBSTR: : Wri teToSt ream. Should they change significantly, this code could break. However, the ccxie is relatively efficient and straightforward to maintain. To use this class in your objects, simply replace the IP ersi stStream­ l n i tlm p l base class w ith a reference to the IP e rs i s tS tre a m ln i tlm p l 2 class. This code cla ss ATL_NO_VTABLE CDemagogue : public IPersistStreamlnitlmpl, public CSupportDi rtyBi t, becomes c la s s ATL_NO_VTABLE CDemagogue •• public IPersistStreamlnitlmpl2 , public CSupportDi rty B it, Another technique for calculating the size of the stream is to save the object to a temporary stream and then retrieve the size of the stream.3 For example, the fol­ lowing class provides a minimal IStream implementation that only tracks how much an object w rites to a stream. c la s s CDummyStream : p u b lic IS tream { p u b lic : CDummyStream() { m—lib S e e k .QuadPart = 0; m_li bMaxOff s e t.QuadPart = 0;} 'Axel Heitlaiui originally inspired this dummy stream approach for calculating the size o f persistent data. 292 ATL INTERNALS STDMETHODIMP Querylnterface (REFIID riid , void^^ppv) { if (riid == IID_I5tream |1 riid =- IID—ISequentialStream || riid == IID^IUnknown) { *ppv = s t a t i c 一 cast (th is ); r e tu r n S一 OK;} return *ppv = NULL, E一NOINTERFACE; } STDMETHODIMP_(ULONC) AddRef() { re tu rn 2; } STDMETHODIMP_(ULONC) Release () { re tu rn 1; } STDMETHODIMP R e a d (vo id * pv, ULONG cb, ULONG* pcbRead) { re tu rn E_NOTIMPL; } STDMETHODIMP W rite (c o n s t v o id * pv, ULONC cb, ULONC* pcbW ritten) { m_li bMaxOffset.QuadPart = max (m_libMaxOffset.QuadPart + cb, m_libSeek.QuadPart + cb); i f (NULL! « pcbW ritten) ☆pcbWritten « cb; //Tell caller only if it cares re tu rn S一OK; } STDMETHODIMP Seek(LARGE_INTEGER dlibM ove, DWORD dwOrigin, ULARGE_INTEGER*plibNewPositi on) { sw itch (dw O rigin) { case STREAM_SEEK_SET: m_li bSeek.QuadPart = d li bMove.QuadPart; b re a k; case STREAM_SEEK_CUR: m_li bSeek.QuadPart += d li bMove.QuadPart; b re a k ; case STREAM_SEE1CEND: m_libSeek.QuadPart = n _li bMaxOffset.Quad P a rt-d li bMove.QuadPart; if (m_libSeek.QuadPart < 0) m一 1ibSeek.QuadPart = 0; b re a k; d e f a u lt: r e tu r n E^UNEXPECTED; } *plibNewPosition = m_libSeek; return S_0K; } STDMETHODIMP SetSize(ULARCE_INTECER libNewSize) { m_libMaxOffset.QuadPart - libNewSize.QuadPart; re tu rn S一 OK; } STDMETHODIMP CopyToCIStream* pstm, ULARCE一 INTEGER cb, PERSISTENCE IN ATL ULARCE_INTECER* pcbRead, ULARGE—INTEGER* p c b W ritte n ) { re tu rn E^NOTIMPL; } STDMETHODIMP Commit(DWORD g rfC o m m itF la g s) { re tu rn E_NOTIMPL; } STDMETHODIMP Revert() { return E.NOTIMPL; } STDMETHODIMP LockRegion(ULARGE一INTEGER T ib O ffs e t, ULARCE_INTECER cb, DWORD dwLockType) { re tu rn E_NOTIMPL; } STDMETHODIMP UnlockRegion(ULARGE_INTEGER li b O f f s e t , ULARGE—INTEGER Cb DWORD dwLockType) { return E_NOTIMPL; } STDMETHODIMP Stat(STATSTG^ pstatstg, DWORD grfS tatFlag) { return E.NOTIMPL; } STDMETHODIMP C lone(IS tream **ppstm ) { re tu rn E_NOTIMPL; } ULARGE_INTECER S ize ( ) { r e tu rn m—lib M a x O ffs e t; } void Clear() { m_libMaxOffset.QuadPart - 0; } ULARGE.INTEGER m J ib S e e k ; ULARCE_INTECER m—lib M a x O ffs e t; You can use it to implement the CetSi zeMax method this way: ' STDMETHODIMP CetSi zeMax(ULARGE.INTECER* pcbSize) { i f (NULL == pcbS ize ) re tu rn E一 POINTER; pcbSize->QuadPart =* 0; CDummyStream ds; HRESULT hr - Save C&ds, FALSE); i f (SUCCEEDED ( h r ) ) ☆pcbSize = ds.SizeC); r e tu rn h r ; } I must caution you to use great care when using the dummy stream approach for calculating the size of an object’s persistent data. There are significant issues in­ volved when the object marshals an interface pointer into the stream as part of its persistent data. The object might do this to pass the interface pointer by reference rather than recursively persisting the referenced object. The dummy stream ap­ proach effectively results in two Save operations (one from the GetSizeMax call and one from the following Save call), which could cause an interface pointer to be marshaled twice. This produces a dangling reference to the stub, which then must be reclaimed by the garbage collection process. 294 ATL INTERNALS Adding Marshal-by-Value Semantics Using Persistence When you pass an interface pointer as a parameter to a remote (out of apartment) method call, the default in COM is to pass by reference. In other words, the object stays where it is and only a reference to the object is given to the recipient of the call. This typically means that references to the object involve round-trips back to the object, which can be quite expensive. An object can override this pass-by- reference default by implementing the I Marshal interface. The primary reason most developers implement IM arshal on an object is to give it pass-by-value semantics. In other words, when you pass an interface pointer to a remote method call, you’d prefer COM to pass a copy of the object to the method. A ll references to the object are then local and do not involve round-trips back to the “original” object. When an object implements IM arshal in a way such that it has pass-by-value semantics, we typically say that the object marshals by value. interface IMarshal : public IUnknown { STOMETHOO CetUrMnarshalClass([in] REFIID r iid . [unique,in] void* pv, [in ] OWORO dwOestContext, [unique,in] void* pvOestContext, [in ] OWORO m shlflags, [out] CLSID* pCid); STOMETHOO GetMarshalSi zeMax([in] REFIID r iid , [unique,in] void* pv, [in ] DWORD dwOestContext, [unique,in] void* pvDestContext, [in ] OWORD nshlflags, [out] OWORD* p S ize ); STDMETHOD M arshallnterface([unique,in] IStream* pStm, [in] REFIID riid , [unique][in] void* pv, [in ] DWORD dwOestContext, [unique,in] void* pvDestContext. [in ] OWORO m shlflags); STOMETHOO UnmarshalInterfaceC[uniquet in ] IStream* pStm, [in] REFIID riid , [out] void** ppv); STOMETHOO Re1easeMarshalData([unique,in] IStream* pStn); STOMETHOO O isconnectO bject([in] OWORO dwReserved); PERSISTENCE IN ATL Given a complete implementation of IP ersi stStream or IP ersi stStream lni t, it’s quite easy to build a marshal-by-value implementation of IM arshal • A class typically implements marshal by value by returning its own CLSID as the result of the CetUnmarshalClass method. The IPersistStream : :Cet- C1 assID method produces the needed CLSID. The GetMarshal Si zeMax method must return the number of bytes needed to save the persistent state of the object into a stream. The IP ersi stS tream :: GetSi zeMax method produces the needed size. The Marshal In te rfa ce and Unmarshal Interface methods need to write and read, respectively, the persistent state of the object into the provided stream. Therefore, I can use the Save and Load methods of IP ersi stStream for this functionality. Rel easeMarshal Data and Di sconnectOb j ect method can simply return S 一 OK. Here's a template class that uses an object’s IP ersi stS tream lni t interface to provide a marshal-by-value implementation.4 Once again, I decided to down-cast and up-cast using s ta ti c_cast to obtain the IP ersi stStream lni t interface so I receive an error at compile time when the deriving class doesn’t implement IPersistStream lnit. template class ATL_NO_VTABLE IMarshalByValuelmpl : public IMarshal { STDMETHODIMP CetUnmarshalClass(REFIID /* riid V , v o id * / * DWORD / * v o id * / * DWORD / * { T* pT = static_cast(this); IPersistStreamlnit* psi = static 一 cast (pT) return psi->CetClassID (pCid); } STDMETHODIMP GetMarshalSi zeMax(REFIID /* r iid * /, v o id * /* pv * / , DWORD /* dwDestContext * /, pv * / , dwDestContext pvDestContext mshlflags * /’ V, V, CLSID *pC id) 4This technique has a history, as does most software development. Jonathon Bordan wrote th e firs t IMarshal ByVal uelmpl after being inspired by a Microsojl System Journal article written by Don Box. Brent Rector then modified Jonathon's example to the present form. ATL INTERNALS void* /* pvDestContext */, DWORO /* m shlflags */, DWORO* pSize) { T* pT = sta ti c_cast(thi s ); IPersistStreamlnit* psi = static一cast (pT); ULARCE_INTEGER u li = { 0 >; HRESULT hr = psi->GetSizeMax(&uli); i f (SUCCEEDED (hr)) *pSize = uli.LowPart; re tu rn h r; } STOMETHODIMP M arshalInterface(IStream *pStm, REFIID /* r i i d * / , v o id * /* pv * / , DWORD /* dwOestContext */» void* /* pvOestCtx */» DWORD /* mshlflags */) { T* pT = s t a t ic 一 cast(this); IPersistStreamlnit* psi = static 一 cast (pT); return psi->Save(pStm, FALSE); } STDMETHODIMP Unmarshal Interface(IS tream ^pStm, REFIID riid , void **ppv) { T* pT = stati c_cast(thi s); IPersi stStreamlni t* psi = static 一cast (pT); HRESULT hr = psi->Load(pStm); if (SUCCEEDED (hr)) hr = pT->QueryInterface (riid , ppv); re tu rn h r; } STDMETHODIMP ReleaseMarshalData(IStream* /* pStm */){ return S_OK;} STDMETHODIMP Di sconnectObj ect(DWORO / * dwReserved * / ) { re tu rn S—OK;} You can use this template class to provide a marshal-by-value implementation for your object. You need to derive your class from the prior IM a rs h a l ByVal - uelmpl class (to get the IMarshal method implementations) and previously described IP e rsi stS tream lni tlm p l 2 class (to get the GetSi zeMax method im­ plementation plus the default implementation of the IPersi stS tream lnit method provided by ATL). You must also add a COM_INTERFACE_ENTRY for IM ar­ shal to the class’s COM map. Here’s an example: PERSISTENCE IN ATL 297 c la s s ATL.NO^VTABLE CDemagogue : 參 • • public IPersi stStreamlni tImpl2, public CSupportDi rtyB it, public IMarshalByValuelmpl〈CDemagogue〉 { BEGIN_COM_MAP(CDemagogue) COM一INTERFACE_ENTRY2(IPersi s t , IPersistStreamlnit) COM一INTERFACE一ENTRYCIPersistStreamlnit) COM^INTERFACE^ENTRV(IMarshal) ENO_COM_MAP() Note that adding marshal-by-value support to your class this way means that all in­ stances of the class use pass-by-value semantics. It. is not possible to pass one ob­ ject instance by reference and another instance by value (assuming both instances have (he same marshaling context一 inproc, local, or different machine). Summary Many objects need some support for persistence, and ATL provides an easily ex­ tensible, table-driven implementation of the IP ersi stStream [In i t] and the IPersistPropertyBag interfaces. These implementations save and load the properties described by the class’s property map entries. By overriding the appro­ priate methods, you can extend this persistence support for data types not sup­ ported by properly map entries. The ATL implementation of IP ersi stStorage suffices to allow the object to be embedded into ar» OLE container but doesn’t take advantage of many of the capabilities of the IS torage medium. Using and extending the stream persistence support provided by ATL allows an object to provide additional functionality to its clients. For example, you’ve seen how to implement IP ersi stMemory support to your object (which MFC-based containers prefer). In addition, you can easily add marshal-by-value semantics to your class by reusing the stream persistence functionality. HAP R Collections and Enumerators Many COM libraries are exposed as sets of objects known as object models. A COM object model is a parent object that holds a set of child objects. COM collections and enumerators art? ( he glue that holds the parent and the children together. This chapter examines COM collections and enumerators and how they work together to build object models. COM Collection and Enumeration Interfaces STL Containers and Iterators STL programmers long ago learned to separate their collections into three pieces: the data itself, the container of the data, and an iterator for accessing the data. This separation is useful for building pieces separately from each other. The container's job is to provide the user the ability to affect the contents of the collection. The it­ erator's job is to allow the user the ability to access the contents of the container. And although the iterator implementation is dependent on how the data is stored by the container, to the client of the container and the iterator, the implementation de­ tails are hidden. For example, imagine the following STL code for populating a con­ tainer and then accessing it via an iterator: void main() { I I Populate the collection vector rgPrimes; for( long n = 0; n i= 1000; ++n ) { if( IsPrime(n) ) rgPrimes.push_back(n); } // Count the number of items in the col lection cout « "Primes: " « rgPrimes.size() « endl; // Iterate over the collection using sequential access vector<1ong>::iterator begin = rgPrimes.begin(); vector::iterator end = rgPrimes.end(); ATL INTERNALS for( vector::iterator it = begin; it != end; ++it ) { cout « *it « ""; } cout « endl; } Because the container provides a well-known C + + interface, the client is excused from knowing the implementation details. In fact, STL container classes are so uni­ form that this simple example would work just as well with a list or a deque as it does with a vector. Likewise, because the iterators provided by the container are uniform, the client doesn’t need to know the implementation details of the iterator. For the client to enjoy these benefits, the container and the iterator have certain responsibilities. The responsibilities of the container include the following. _ The container may allow the user to manipulate the data. Most containers are of variable size and are populated by the client. However, some containers may represent a fixed data set or a set of data that is calculated instead of stored. ■ The container may allow the user to obtain the count of items. STL containers have a size method for this purpose. ■ The container may allow random access. The STL vector allows this using operator [ ] ,whereas the STL list does not ■ The container must allow the user to access the data at least sequentially, if not randomly. STL containers provide this facility by exposing iterators. Likewise, the responsibilities of the iterator entail the following. ■ The iterator must be able to access the container’s data. That data may be in some shared spot (for example, memory, file, or database) where the collection and iterator can both access the data. Or, the iterator may have its own copy of the data. This would allow one client to access a snapshot of the data while an­ other client modified the data using the container. Finally, the iterator could generate the data on demand, for example, an iterator that generates the next prime number. ■ The iterator must keep track of its current position in the collection of data. Every call to the iterator's ope rator++ means to advance that position. Every call to the iterator’s operator* means to hand out the data at the current position. ■ The iterator must be able to indicate the end of the container to the client. COLLECTIONS AND ENUMERATORS While C++ containers and STL-like iterators are handy in your C + + code, neither is useful as a way for communicating data via a COM interface. Instead, we turn to the COM equivalent of containers and iterators: COM collections and enumerators. COM Collections and Enumerators A COM collecLion is a COM object that holds a set of data and allows the client to manipulate its contents via a COM interface. In many ways, a COM collection is very similar to an STL container. By convention, a COM collection interface takes a min­ imum form. This form is shown below, pretending that Interface Definition Lan­ guage (IDL) supports templates: [ dual ] template interface ICollection : IDispatch { [propget] HRESULT Count([out, retval] long* pnCount); [id(DISPID_VALUE)t propget] HRESULT ItemC[in] long n, [out, retval] T* pnltem); [id(DISPID NEWENUM), propget] HRESULT _NewEnum([out, retval] IUnknown** ppEnum); }; There are several features about this interface worth noting, as follows. ■ Although this minimal collection interface doesn’t show any methods for add­ ing or removing elements from the collection, most collections include such methods. ■ Most collection interfaces are dual interfaces. A nlD i spatch-based interface is required for some convenient language-mapping features that I ,ll discuss later. _ Most collection interfaces have a read-only Count property that provides a count of the current elements in the collection. Not all collections can calculate a reliable count, however. Examples include a collection of all prime numbers and a collection of rows from a database query that hasn't yet been completed. ■ Most collection interfaces have a read-only Item property for random access to a specific element. The first parameter is the index of the element to access, AT L INTERNALS which I’ve shown as a 1 ong. It’s also common for this to be a VARIANT,so that a number index or a string name can be used. If the index is a number, it is often 1-based. Further, the Item property should be given the standard DISPIL) DISPID—VALUE. This marks the property as the “default” property, which cer­ tain language mappings use to provide more convenient access. 1,11 show you how this works later. ■ What makes a collection interface is exposing an enumerator via the read-only property _NewEnum, which niust be assigned the standard DISF1D: DISPID 一 NEWENUM. This DISPID is used by Visual Basic to implement its For-Each syn- tax, as I ,ll show you soon. A COM enumerator is to a COM collection as an STL iterator is to an STL con­ tainer. The collection holds the data and allows the client to manipulate it, and the enumerator allows the client sequential access. However, instead of providing se­ quential access one element at a time, like an iterator, an enumerator allows the client to decide how many elements it would like. This gives the client the ability to balance the cost of round-trips with the memory requirements to handle more ele­ ments at once. A COM enumerator interface takes the following form (again, pre­ tending that IDL supports templates): template interface IEnum : IUnknown { [local ] HRESULT Next([in] ULONC celt, [out] T* rgelt, [out] ULONG ^pceltFetched); [call^as(Next)] // Later... HRESULT RemoteNext([in] ULONG celt, [out, size 一 is(celt〕 , length 一 is(*pceltFetched〕] T* rgelt, [out] ULONC ^pceltFetched); HRESULT Skip([in] ULONG celt); HRESULT Reset(); HRESULT Clone([out] IEnum **ppenum); A COM enumerator interface has the following properties. COLLECTIONS AND ENUMERATORS 303 ■ The enumerator must be able to access the data of the collection as well as maintain a logical pointer to the next element to retrieve. All operations on an enumerator manage this logical pointer in some manner. ■ The Next method allows the client to decide how many elements to retrieve in a single round-trip. A result of S—OK indicates that the exact number of elements requested by the c e lt parameter has been returned in the r g e lt array. A re­ sult of S_FALSE indicates that the end of the collection has been reached and that the peel tFetched argument holds the number of elements actually re­ trieved. In addition to retrieving the elements, the Next method implementa­ tion must advance the logical pointer internally so that subsequent calls to Next can retrieve additional data. ■ The Skip method moves the logical pointer but retrieves no data. Notice that c e lt is an unsi gned 1 ong, so there is no skipping backward. You can think of ai\ enumerator as modeling a single-linked list, although, of course, it can be im­ plemented any number of ways. ■ The Reset method moves the logical pointer back to the beginning of the collection. ■ The Clone method returns a copy of the enumerator object. The copy refers to the same data (although it may have iia own copy) and points to the same logi­ cal position in the collection. The combination of Skip, Reset, and Clone make up for the lack of a Back method. Custom Collection and Enumerator Example For example, let's model a collection of prime numbers as a COM collection: [dual] interface IPrimeNumbers : IDispatch { HRESULT CalcPrimes([in] long min, [in] long max); [propget] HRESULT Count([out, retval] long* pnCount); [propget, 1d(DISPID__VALUE)] HRESULT Item([in] long n , [out, retval] long* pnPrime); [propget, id(DISPID_NEWENUM)] // Not quite right... HRESULT —NewEnum([out, retval] IEnumPrimes** ppEnumPrimes); ATL INTERNALS The corresponding enumerator would look like this: interface IEnumPrimes : IUnknown { [local] HRESULT Next([in] ULONG celt, [out] long* rgelt, [out] ULONG *pceltFetched); [call_as(Next)] HRESULT RemoteNext([in] ULONG celt, [out, size_is(celt), length_is(*pce1tFetched)] long* rgelt, [out] ULONG *pceltFetched); HRESULT SkipCCin] ULONC celt); HRESULT ResetO; HRESULT Clone([out] IEnumPrimes ^^ppenum); Porting the previous STL client to use the collection and enumerator would look like this: void main() { CoInitialize(O); CComPtr spPrimes; if( SUCCEEDED(spPrimes.CoCreateInstance(CLSID_PrimeNumbers))) { // Populate the collection HRESULT hr; hr = spPrimes->CalcPrimes(0, 1000); // Count the number of items in the collection long nPrimes; hr = spPrimes->get_Count(&nPrimes); cout « "Primes: •• « nPrimes « endl; // Enumerate over the collection using sequential access CComPtr spEnum; hr = spPrimes->get_ NewEnum(&spEnum); COLLECTIONS AND ENUMERATORS const size.t PRIMES_CHUNK = 64; long rgnPrimes[PRIMES—CHUNK]; do { ULONC celtFetched; hr = spEnum->Next(PRIMES_CHUNK, rgnPrimes, &celtFetched); if( SUCCEEDED(hr) ) { if( hr == S_OK ) celtFetched - PRIMES.CHUNK; for( long* pn = &rgnPrimes[0]; pn != &rgnPrimes[celtFetched]; ++pn ) { cout « *pn « ""; whileC hr -= S—OK ); cout « end!; spPrimes. ReleaseO; } CoUninitializeO; } This client code asks the collection object to populate itself via the CalcPrimes method instead of adding each prime number one at a time. Of course, this proce­ dure reduces round-trips. The client further reduces round-trips when retrieving the data in chunks of 64 elements. A chunk size of any number greater than 1 reduces round-trips but increases the data requirement of the client. Only profiling can tell you the right number for each client/enumerator pair, but larger numbers are pre­ ferred to reduce round-trips. Dealing with the Enumerator local/call_as Oddity One thing that’s rather odd about the client side of enumeration is the pcelt- Fetched parameter filled by the Next method. The COM documentation is am­ biguous, but it boils down to this: When only a single element is requested, the client doesn't have to provide storage for the number of elements fetched; that is, peel t - Fetched is allowed to be NULL. Normally, however, MIDL (ioesn’t allow an [out] parameter to be NULL. So, to support the documented behavior for enumeration in­ terfaces, all of them are defined with two versions of the Next method. The [ lo ­ cal] Next method is for use by the client and allows the peel tFetched parameter 306 ATL INTERNALS Client IEnumPrimes, Obiect P»o«y IEnumPrimes_Next_Proxy )lEnumPrifnes_RenK)teNext_Pro)ty| IEnumPrimes^Next_Stub ] IEnumPrimes RemoteNext_Stub Figure 7.1. The call progression from client, through proxy and stub, to imple­ mentation of IEnumPri mes. to be NULL. The [call_as] RemoteNext method doesn't allow the peel tFetched parameter to be NULL and is the method that performs the marshaling. Although the MIDL compiler will implement the RemoteNext method, since we’ve marked the Next method as [local] we have to implement Next manually. In fact, we’re re­ sponsible for implementing two versions of the Next method. One version is called by the client and will in turn call the RemoteNext method implemented by the proxy. The other version is called by the stub and will call the Next method imple­ mented by the object Figure 7.1 shows the progression of calls from client to object through the proxy, the stub, and our custom code. The canonical implementation is as follows: static HRESULT STDMETHODCALLTYPE IEnuwPriiws 一 Next—Proxy ( IEnumPriaes* This, ULONC celt, long* rgelt, ULONC* peeltFetched) { ULONC cFetched; if( 1 peeltFetched && celt !- 1 ) return E^INVALIDARC; return IEnu»Pr1nes_RenioteNext_Proxy(This, celt, rgelt, peeltFetched ? peeltFetched : AcFetched); } static HRESULT STDMETHODCALLTYPE IEnu*Pri»es_Next-Stub( IEmmPrfiics* This, ULONG celt, long* rgelt, ULONC* peel tFetched) { HRESULT hr ■ This->lpVtbl->Next(This, celt, rgelt, peeltFetched); COLLECTIONS AND ENUMERATORS if( hr == S—OK && celt =*: 1 ) ☆pceltFetched = 1; return hr; Every enumeration interface includes this code in the proxy/stub implementa­ tion, including all the standard ones, such as IEnumllnknown, IEnumStri ng, and IEnumVARIANT. The only difference in implementation is the name of the inter­ face and the type of data being enumerated over (as shown in the IEnumPrimes example in bold). When you’re building the proxy/stub for your project using the ps .mk file generated by the wizard, and you have a custom enumeration interface, it’s your job to inject, that code into your proxy/stub. One way would be to edit the -p.c fUe, but if you were to recompile the IDL, the implementation would be lost. Another way would be to add another • c file to the proxy/stub proj­ ect, but since this would involve manually editing the makefile (a skill I’d rather not reacquire), this is rather unpleasant. The technique I prefer relies on macro defini­ tions used during the proxy/stub building process and makes heavy use of the cpp_quote statement in IDL.1 Whenever you have a custom enumeration interface, insert code like this at the bottom of the IDL file and all will be right with the world (the bold code changes based on the enumeration interface): (^p_quote("#i fdef —midl—proxy") cpp_quote("static HRESULT STDMETHODCALLTYPE") cpp_quote("IEnumPrimes_Next_Proxy") cpp_quote("(IEnumPrimes* This, ULONG celt, long* rgelt, ULONG* peeltFetched)") cpp-qu0te(”{") cpp_quote(" ULONC cFetched;") cpp.quoteC" if( ! peel tFetched SA celt != 1 ) return E.INVALIDARC;'*) cpp 一 quote(” return IEnuwPri«es_RemoteNext_Proxy(This, celt, rgelt,") cpp_quote(n peeltFetched ? peeltFetched : &cFetched);") cpp一 quote ( " } ”) cpp_quote("") cpp—quote("static HRESULT STDMETHODCALLTYPE") cpp_quote(MIEnumPrimes_Next_Stub") cpp_quote("(IEnumPrimes* This, ULONC celt, long* rgelt, ULONC* pceltFetched)") 11 learned these tricks from Don Box and his enumeration generation macros. As of the time of this writ­ ing, these macros were available at http://www.develop.eom/dbox/com/enumgen.h. ATL INTERNALS cpp_quote("r) cpp.quoteC' HRESULT hr cpp_quote(" This->lpVtbl->Next(This, celt, rgelt,") pceltFetched); ••) cpp_quote(" if( hr == S_0K && celt == 1 ) ^pceltFetched = 1;") cpp_quote(' return hr;") cpp_quoteC,'}M) cpp_quote("#endif // 一 midi 一 proxy") All the code within the cpp—quote statements will be deposited into the .h file, but, because of the use of the _ m idl_proxy symbol, will only be compiled when building the proxy/stub. An Enumeration Iterator One other niggling problem with COM enumerators is their ease of use, or rather, the lack thereof. While it’s good that a client has control of the number of elements to retrieve in a single round-trip, logically the client is still processing the data one element at a time. This is obfuscated by the fact that we’re using two loops instead of one. Of course, C++ being C 卞 +,there’s no reason a wrapper can’t be built that removes this obfuscation.2 Such a wrapper is included with the source code ex­ amples of this book.3 It’s called the enum一 i te r a to r and is declared like this: #ifndef ENUM_CHUNK #define ENUM_CHUN< 64 #endif template > COLLECTIONS AND ENUMERATORS 309 enum_iterator& operator— 。 ; enum」 terator operator++(int); EnumType& operator*(); private: The enum_i te r a to r class provides an STL-like forward iterator that wraps a COM enumerator. The type of the enumeration interface and the type of data that it enumerates are specified as template parameters. The buffer size is passed, along with the pointer to the enumeration interface, as a constructor argument. The first constructor allows for the common usage of forward iterators. Instead of asking a container for the beginning and ending iterators, the beginning iterator is created by passing a non-NULL enumeration interface pointer. The ending iterator is created by passing NULL. The copy constructor is used when forming a looping statement. This iterator considerably simplifies the client enumeration code: • • • // Enumerate over the collection using sequential access CComPtr spEnum; hr = spPrimes->get_ NewEnum(&spEnum); // Using an STL-1 ike forward iterator typedef enum_iterator primes_iterator; primes_i terator beginCspEnum, 64); primes_iterator end; for(primes_iterator it = begin; it != end; ++it ) { cout « > cout « endl; 參■參 Or, if you'd like to get a little fancier, you could use the enum一 ite r a to r with a function object and an STL algorithm, which allows you to avoid writing the loop­ ing code altogether. For example: struct OutputPrime { void operator()(const long& nPrime) { ATL INTERNALS cout « nPrime « ” "; // Using an STL algorithm typedef enum_iterator primes_iterator; for_each(primes_iterator(spEnum, 64), primes^iterator(), OutputPrimeO); • • • Although this example may not be as clear to you as the looping example, it warms the cockles of my STL heart.4 Enumeration and Visual Basic The STL fo r 一 each algorithm may seem quite like the Visual Basic (VB) For-Each statement, and it is. The For-Each statement allows a VB programmer to access each element in a collection, whether it’s an intrinsic collection built into VB or a custom collection developed using COM. Just as the for_each algorithm is im­ plemented using iterators, the For-Each syntax is implemented using a COM enumerator, specifically, IEnumVARIANT. To support the For-Each syntax, the collection interface must be IDi spatch-based and have the 一 NewEnum property marked with the DISPID_NEWENUM DISPID. Because our prime number collection object exposes such a method, you may be tempted to write the following code to exercise the For-Each statement: Private Sub Commandl_Click() Dim primes As IPrimeNumbers Set primes = New PrimeNumbers primes.CalcPrines 0, 1000 MsgBox "Primes: " & primes.Count Dim sPrimes As String Dim prime As Variant 41 should mention that it warms the cookies of Chris's STL heart. Brent's heart has no such cockles. COLLECTIONS AND ENUMERATORS 311 For Each prime In primes • Calls Invoke(DISPID_NEWENUM) sPrimes * sPrimes & prime & •• Next prime MsgBox sPrimes End Sub When VB sees the For-Each statement, it will invoke the —NewEnum property, look­ ing for an enumerator that implements IEnumVARIANT. To support this usage, our prime number collection interface must change from exposing IEnumPri mes to ex­ posing IEnumVARIANT. Here’s the twist: The signature of the method is actually ^NewEnum(IUnknown**), not. _NewEnum(IEnumVARIANT**). VB will take the IUnknown* returned from —NewEnum and query for IEnumVARIANT. It would’ve been nice for VB to avoid an extra round-trip, but maybe the VB team expects to support other enumeration types in the future. Modifying IPrimeNumbers to support the VB For-Each syntax looks like this: [dual] interface IPrimeNunbers : IDispatch { HRESULT CalcPrimes([in] long min, [in] long max); [propget] HRESULT Count([out, retval] long* pnCount); [propget, id(DISPID_VALUE)] HRESULT Item([in] long n, [out, retval] long* pnPrime); [propget, id(DISPID_NEWENUM)] HRESULT .NewEnum([out, retval] IUnknown** ppunkEnun); This brings the IPrimeNumbers interface into line with the ICol 1 ec tio n template form I showed you earlier. In fact, it’s fair to say that the ICol 1 e c t i o n template form was defined to work with VB. The VB Subscript Operator Using the Item method, a VB client can access each individuaJ item in the collec­ tion one at a time: 312 ATL INTERNALS Dim i As Long For i = 1 To primes.Count sPrimes * sPrirres & primes.Item(i) & " " Next i • t • Because 1 marked the Item method with DISPID_VALUE, VB allows the fol­ lowing abbreviated syntax that makes a collection seem like an array (if only for a second): • • • Dim i As Long For i = 1 To primes.Count sPrimes = sPrines & primes(i) & ..... Invoke(OISPID_VALUE) Next i Assigning a property the DISPID_VALUE dispatch identifier makes it the default property as far as VB is concerned. Using this syntax results in VB getting the de­ fault property, that is, calling Invoke with DISPID_VALUE. However, because we’re dealing with array syntax in VB, we have two problems. The first is knowing where to start the index— 1 or 0? A majority of existing code suggests making col­ lections 1-based, but only a slight majority. As a collection implementor, you get to choose. As a collection user, you get to guess. The other concern with using array-style access is round-trips. Using the Item property puts us smack dab into the middle of what we’re trying to avoid by using enumerators: one round-trip per data element. If you think that using the For-Each statement, and therefore enumerators, under VB would solve both of these prob­ lems, you’re half right. Unfortunately, as of this writing, Visual Basic Enterprise Edition 6.0 continues to access elements one at a time, even though it’s using IEnumVARIANT:: Next and is perfectly capable of providing a larger buffer. How­ ever, using the For-Each syntax does allow you to disregard whether the Item method is 1-based or 0-based. The Server Side of Enumeration Of course, because the semantics of enumeration interfaces are loose, you are free to implement them however you like. The data could be pulled from an array, a file, a database result set, or wherever it may be stored. Even better, you might want to COLLECTIONS AND ENUMERATORS 313 calculate the data on demand, saving yourself calculations and storage for elements in which the client isn’t interested. Either way, if you’re doing it by hand, you have some COM grunge code to write. Or, if you like, ATL is there to help write that grunge code. Enumerating Arrays CComEnum Because enumeration interfaces are all the same except for the actual data being enumerated, their implementation can be standardized, given a couple of assump­ tions. Depending on how you’ve stored your data, you can use one of two ATL enumeration interface classes. The most flexible implementation class allows you to provide your data in an STL-like collection. This is called CComEnumOnSTL, which I’ll present later. The simplest implementation assumes you’ve stored your data as an array. It’s called CComEnum, and the complete implementation is as follows: tempiate class ATUNO.VTABLE CComEnum : public CComEnumlmpl<6ase, pi id, T, Copy>, public CComObjectRootEx< ThreadModel > { public: typedef CComEnum .CComEnum; typedef CComEnumlmpl .CComEnumBase BECIN_COM.MAPCCComEnum) COM-INTERFACE_ENTRY^IID(*pi i d t .CComEnumBase) ENO_COM_MAP() Although this implementation only comprises a few lines of code, there’s quite a lot going on here. The template arguments are as follows: ■ Base is the enumeration interface to be implemented, for example, IEnum­ Primes. ■ P"iid is a pointer to the interface being implemented, for example, &IID 一 IEnumPrimes. ■ T is the type of data being enumerated, for example, 1 ong. ATL INTERNALS ■ Copy is the class responsible for copying the data into the client’s buffer as part of the implementation of Next. It may also be used to cache a private copy of the data in the enumerator to guard against simultaneous access and manipulation. ■ ThreadModel describes just how thread safe this enumerator needs to be. When you specify nothing, it will use the dominant threading model for objects, as described in Chapter 3. Of course, because a COM enumerator is a COM ob­ ject like any other, it requires an implementation of IUnknown. Toward that end. CComEnum derives from CComOb jectRootEx. You’ll see later that I’ll fur­ ther derive CComOb je c t from CComEnum to fill in the vtbl properly. Really, CComEnum is present simply to bring CComOb j ectRootEx together with CComEnumlmpl, the base class that actually implements Next, Skip, Reset, and Clone. Figure 7.2 shows how these classes fit together. Copy Policy Classes The fundamental job of the enumerator is to copy the collection’s data into the buffer provided by the client. If the data being enumerated is a pointer or a struc­ ture that contains pointers, a simple memcpy or assignment is not going to do the < $ > < j > Base inh,erits- £ CCComEnumlmpkBase, piid, T, C o p ^ C^C^mObj0CtRcx)tEx^> 、 丨卟曰乂 、^ 一 CCComEnum^ CCComObject>> Figure 7.2. The CComEnum inheritance hierarchy COLLECTIONS AND ENUMERATORS 315 trick. Instead, the client is going to need its own deep copy of each element, which it can release when it has finished with it. Toward that end, ATL enumerators use a class called a copy policy class, or often just a copy policy, to scope static methods for dealing with deep-copy semantics. The static methods of 压 copy policy are like the Increment and Decrement methods of the threading model classes, except that instead of incrementing and decrementing a 1 ong, copy policies know how to initialize, copy, and destroy data. For simple types, ATL provides a template copy policy class: template class -Copy { public: static HRESULT copy(T* pi, T* p2) { memcpy(pi, p2, sizeof(T)); return S.OK; static void initCT*) {> static void destroy(T*) {> Given an array of a simple type (such as long), this template would work ju st fine: HRESULT CopyRange(long* dest, long* src, size一t count) { for( size_t i =0; i !« count; ++i ) { HRESULT hr - _Copy<1on9>: :copy(Mest[1] • &src[i]); if( FAILED(hr) ) { while( i > 0 ) _Copy<1ong>::destroy(&d€St[—1]); return hr; > } return S—OK; However, given something with trickier semantics, such as a VARIANT or an OLESTR, memcpy is too shallow. For the four most commonly enumerated data types, ATL provides specializations of the 一Copy template: tempiateo class .Copy ; templateo class _Copy; tempi at e o class .Copy<0t£VERB> templateo class ^Copy; 316 ATL INTERNALS For example, the copy policy for VARIANTS looks like this: tempi ateo class _Copy { public: static HRESULT copy(VARIANT* pi, VARIANT* p2) { return VariantCopy(pl, p2); } static void init(VARIANT* p) { p->vt = VT^EHPTY; } static void destroy(VARIANT* p) { VariantClear(p); } }; If you're dealing with interface pointers, again, the -Copy template won’t do, but building your own specialization for each interface you’d like to copy is a bit arduous. For interfaces, ATL provides the 一 Copylnterface copy policy class pa­ rameterized on the type of interface you’re managing: template《h s s T> class .Copylnttrfacc { public: static HRESULT copy(T** pi, T** p2) { *pl « *p2; if (*pl) (*pl)->AddRef(); return S—OK; } static void init(T** ) {} static void destroy(T** p) { 1f (*p) (*p)*>Release(); > Using copy policies, we now have a generic way to initialize, copy, and delete any kind of data, making it easy to build a generic and safe duplication routine: template HRESULT CopyRangeCT* dest, T* src, size_t count) { for( size_t i - 0 ; 彳 !* count; ++1 ) { HRESULT hr *» Copy:: copy (M e s t[1] • Asrc[1]); if( FAILEDChr) ) { whileC i > 0 ) Cbpy::destroy(Ad«st[—1]); return hr; return S_0K; COLLECTIONS AND ENUMERATORS CComEnumlmpl’s implementation of the Next method uses the copy policy passed as the template parameter to initialize the client’s buffer and fill it with data from the collection, very much like my sample CopyRange routine. However, be­ fore we jump right into the Next method, let's see how CComEnumlmpl does its job. CComEnumlmpl To implement the methods of an enumeration interface, CComEnumlmpl maintains five data members: template class ATL_N0..VTABLE CComEnumlmpl : publ ic Base { public: CComEnumlmpl(); 〜CComEnumlmpl(); STDMETHOO(Next)(ULONC celt, T* rgelt, ULONC* pceltFetched); STDMETHOOCSkip)(ULONC celt); STDMETHOD(Reset)(void) STDMETHOOCClone)(Base** ppEnum); HRESULT Ini t(T* begin, T* end, IUnknown* pUnk, CComEnumFlags flags = At!FlagNoCopy); T* m„begin; T* m_end; T* waiter; DWORD m_dwFlags; CComPtr m_spUnk; The m一 begin, m_end, and m _iter members are each pointers to the type of data being enumerated, as passed via the T template parameter. Each of t hese mem­ bers keeps track of pointers into an array of the data being enumerated. In classic STL style, m一 begi n points to the beginning of the array, m一 end points to one past the end of the array, and m _iter points to the next element, to hand out. The m_dwFl ags member determines if and when to copy initialization data provided by the creator of the enumerator. The m—spUnk member refers to the owner of the data if the enumerator is sharing it instead of keeping its own copy. The implementations 318 ATL INTERNALS of Next, Skip, Reset, and Clone use these variables to provide their behavior. These variables are set in the In i t method of CComEnumlmpl. Initializing CComEnumlmpl Calling the In i t methodr> requires that the data has been arranged into an array. Maybe the collection is already maintaining the data as an array, or maybe it’s not. Either way, the begi n parameter to In i t must be a pointer to the beginning of an array of the type being enumerated, and the end parameter must be one past the end of the same array. Where that array comes from and how it’s managed by the enu­ merator depends on the last parameter to In i t, the fla g s parameter, which can take one of three values: _ A tl FI agNoCopy means that the collection already maintains its data in an ar­ ray of the type being enumerated and is willing to share the data with the enu­ merator. This is more efficient, because the enumerator doesn't keep its own copy, but. merely initializes m_begin, m_end, and m_i te r to point at the col­ lection's data. However, this could lead to unpredictable results if a client uses the collection to modify the data while it's being enumerated. If you use the Atl FI agNoCopy flag, you should pass an interface pointer to the collect ion that owns the data as the pUnk parameter to In i t. The enumer­ ator will cache this interface pointer, adding to the reference count of the col­ lection. This is necessary to avoid an enumerator outliving the collection and, more imponant, the data that the collection is maintaining. For each of the other two flags, pUnk will be NULL. ■ A tl FI agCopy means that, the collection already maintains the data in the ap­ propriate format but would prefer the enumerator to have its own copy of the data.下his is less efticient, but will ensure that no manipulation of the collection will affect the data maintained by the enumerator. ■ A tl FI agTakeOwnershi p means that the collection doesn't maintain its data in an array of a type appropriate for use by the enumerator. Instead, the collection has allocated an array of the data type being enumerated using operator new for solo use of the enumerator. When the enumerator is destroyed, it should de­ stroy its copy of the data using operator del ete. This is especially handy for the implementation of IEnumVARIANT, because most developers prefer to keep data in types more specific than VARIANT but are willing to provide an array of VARIANTS when creating the enumerator. ''As of this writing, the Ini t methods of both CComEnumlmpl and IEnumOnSTLImpI are absolutely essential to using ATL enumerutor Ln\plement.ation.s and are totally undocumented. COLLECTIONS AND ENUMERATORS CComEnumlmpl Implementation The n\ost interesting part of the CComEnumlmpl implemer\tation is the Next method. Recall (hat Nexts job is to copy the client-requested number of elements into the client-provided buffer. CComEnumlmpl's implementation of the Next method is identical in concept to the CopyRange function I showed you earlier. Next uses the copy policy to copy the data provided by the collection at initializa­ tion into (he client's buffer. If anything goes wrong, the copy policy is used to de­ stroy the data already copied. The rest of the logic is argument validation and watching for the end of the data. template STDMETHOOIMP CComEnumlmpl::Next(ULONC celt, T* rgelt, ULONG* pceltFetched) { if (rgelt == NULL || (celt != 1 && pceltFetched == NULL)) return E_POINTER; if (m—begin == NULL JI m_end == NULL || m_iter == NULL) return E_FAIL; ULONC nRem = (ULONC)(m_end-m_iter); HRESULT hRes = S—OK; if (nRem < celt) hRes = S_FALSE; ULONC nMin = min(celt, nRem); if (pceltFetched !- NULL) *pceltFetched = nMin; T* pelt = rgelt; while (nMin--) { HRESULT hr = Copy::copy(pelt, m_iter); if (FAILED(hr)) { while (rgelt < pelt) Copy::destroy(rgelt++); if (pceltFetched != NULL) ☆peeltFetched = 0; return hr; pelt++; } return hRes; 320 ATL INTERNALS The implementations of Ski p and Reset are trivial: template STDMETHODIMP CComEnumlmpl::Skip(ULONG celt) { m_iter += celt; if (m_iter <= m_end) return S_0K; m_iter = m_end; return S.FALSE; } template STDMETHODIMP CComEnumlmpl::Reset() { m_i ter = : m_begin; return S_0K; } The Clone method is responsible for duplicating the current enumerator. This means creating a new enumerator of the same type and initializing it using the In i t method. However, the data is never copied again for subsequent enumerators. In­ stead, if the collection indicated that the data was to be shared, a new enumerator gets the IUnknown* of the original collection, giving the collection another reason to live. Otherwise, if the enumerator is keeping its own copy of the data, the new enumerator is given the IUnknown* of the original enumerator. Since enumerators are read-only, one copy of the data serves for all enumerators. template STDMETHODIMP CComEnumlmpl〈Base, pi id, T, Copy>::Clone(Base** ppEnum) { typedef CComObjecc > 一 cl ass; HRESULT hRes = E—POINTER; if (ppEnum != NULL) { ☆ppEnum = NULL; _class* p; hRes = _class::CreateInstance(&p); if (SUCCEEDED(hRes)) { // If the data is a copy then we need to keep "this" object // around hRes = p->Init(m一 begin, m 一end, (m_dwFlags & BitCopy) ? this : m_spUnk); if (SUCCEEDED(hRes)) { p->ra_iter = m一 iter; HRes = p->_InternalQueryInterface(*piid, (void**)ppEnum); } COLLECTIONS AND ENUMERATORS if (FAILED(hRes)) delete p; > } return hRes; } CComEnum Usage As'an example of a typical CComEnum usage, let's implement the IPrimeNumbers collection interface: fduall interface IPrimeNumbers : IDispatch { HRESULT CalcPrimes([in] long min, [in: long max); [propget] HRESULT Count([out, retval] long* pnCount); [propget, id(DISPID_VALUE)] HRESULT Item([in] long n, [out, retval] longfr pnPrime); [propget, id(DISPID_NEWENUM)] HRESULT _NewEnum([out, retval] IUnknown** ppunkEnum); The collection maintains a list of the prime numbers in an STL vector. The C alc­ Primes method populates the collection: STDMETHODIMP CPrimeNumbers::CalcPrimes(long min, long max) { m_rgPrimes.clear(); for( long n = min; n <= max; ++n ) { if( IsPrime(n) ) m_rgPrimes.push_back(n); } return S—OK; } The get_Count and get—Item methods use the vector to perform their duties: STDMETHODIMP CPrimeNumbers::get_Count(long* pnCount) { *pnCount - m_rgPrimes.size(); return S一OK; ATL INTERNALS STOMETHODIMP CPrimeNumbers::get_Item(long n , long* pnPrine) { //Oh, let's be 1-based today... ifC n < 1 11 n > rn_rgPrinies.size() ) return E^INVALIDARC; *pnPrime = m_rgPrimes[n-l]; return S一 OK; } Because we’re going out of our way to support VB with our collection inter­ face, the get— NewEnum method will return an interface on an implementation of IEnumVARIANT. Since the name of the parameterized enumerator is used more than once, it’s often handy to use a type definition: typedef CComEnum< IEnumVARIANT, &IID_IEnumVARIANT, VARIANT, _Copy > CComEnumVariant; Remember, the CComEnum template parameters are, in order, the interface we’d like the enumerator to implement, the IID of that interface, the type of data we’d like to enumerate, and finally, a copy policy class for copying the data from the enumerators copy to the client’s buffer. To provide an implementation of IUn­ known, the CComEnum class is further used as the base class for a new CCom- O bject class. Using this type definition, the implementation of get— NewEnum entails creating an instance of an enumerator, initializing it with array data, and filling ppunkEnum with a pointer to the enumerator for use by the client. Because we’re keeping the data as a vector, however, we’re going to have to allocate an ar­ ray of VARIANTS manually, fill the data from the vector, and pass ownership to the enumeration using A tl FI agTakeOwnershi p. The following code illustrates this procedure: STDMETHODIMP CPrimeNumbers::get— NewEnum(IUnknown** ppunkEnum) { ☆ppunkEnum = 0 ; // Create an instance of the enumerator CComObject* pe = 0; HRESULT hr = CComObject::Createlnstance(Ape); if( SUCCEEDED(hr) ) { pe->AddRef(); II Copy data from vector to VARIANT* size_t nPrimes = m_rgPrimes.size(); COLLECTIONS AND ENUMERATORS 323 return hr; Unfortunately, this code leaves an unpleasant taste in one’s mouth. While it would have been considerably simpler if we'd already had an array of VARIANTS holding the data, frankly that's rare. C + 十 programmers tend to use containers other than the error-prone C+ + array. Because of this tendency, we were forced to translate the data from our preferred format to the preferred format of the ATL enu­ merator implementation. Given the regularity of an STL container's C+ + interface, this seems like a waste. In an ideal world, we d have an enumeration implementa­ tion that could handle an STL container instead of an array. In an ideal world, we’d have CComEnumOnSTL. Welcome to niy ideal world.... VARIANT* rgvar = new VARIANT[nPrimes]; if( rgvar ) { ZeroMemoryCrgvar, sizeof(VARIANT) * nPrimes); VARIANT* pvar = &rgvar[0]; for( vec.tor::iterator it = m_rgPrimes.begin(); it != m_rgPrimes.end(); +-+pvar, ) { pvar->vt = VT_I4; pvar->lVa1 = *it; } // Initialize enumerator hr » pe->Init(&rgvar[0], &rgvar[nPrimes], 0 , AtlFIagTakeOwnership); if( SUCCEEDEO(hr) ) { II Fill outbound parameter hr = pe->QueryInterface(IIO_IUnknown, (void**)ppunkEnum); } } else { hr ^ E.0UT0FMF.M0RY; 324 ATL INTERNALS Enumerating STL Collections CComEnumOnSTL The declaration of CComEnumOnSTL is similar to CComEnum: template public: typedef CComEnumOnSTL —CComEnum; typedef IEnumOnSTLImpl •CComEnumBase; B£CIN.COK-MAPCCComEnum) COM_INTERFACE^ENTRY.IIDC*piid, .CComEnumBase) £ND^COMJ class CollType { publi c: class Iterator; // Forward declaration iterator begin(); iterator end(); class iterator { publi c : iterator(const iteratorA it); // To support postfix +♦ iteratorA operator镛(const iteratorA 1t); bool operator!-(const iterator^ rhs); T4 operator*Q; Class ATl^NO.VTABLE CComE L : public IEnuaOnSTLIapI public CComObjectRootEx< ThreadModel > COLLECTIONS AND ENUMERATORS 325 iterator operator++(int); // Postfix ++ All existing STL collections adhere to these minimum requirements with the sole exception of maps. (Maps are the exception as their iterator, when dereferenced, yields a std :: pair, not the contained type.) 1,11 show you later how defining your own collection type is useful for enumerating data calculated on demand. IEnumOnSTLImpl The base class of CComEnumOnSTL, IEnumOnSTLImpl, uses the STLrlike collection passed as the CollType parameter to implement the Next, Skip, Reset, and Cl one methods. The following is the declaration of IEnumOnSTLImpl: template class ATL_NO_VTABLE IEnuinOnSTLIiRpI : public Base { public: HRESULT Init(IUnknown *pUnkForRelease, CollTypeA collection); STOHETHOOCNext)(ULONC celt. T* rgelt, ULONC* pceltFetched); STDMETHOOCSkip)(ULONC celt); STOMETHOO(Reset)(void); STDMETHOOCClone)(Base** ppEnum); //Data S 雜 | CComPtr nuspUnk; CollType* m^pcollection; CollType::iterator m_iter; Just like CComEnumlmpl, IEnumOnSTLImpl keeps an m_spUnk pointer. How­ ever, unlike CComEnumlmpl, the m_spUnk pointer should never be NULL and there­ fore the pUnkForRelease parameter to I n i t should never be NULL Notice that IEnumOnSTLImpl keeps no rrudwFl ags member data. It has no option for copying the data from the collection. Instead, it needs to ensure that the collection holding the data outlives the enumerator. Every call to In i t assumes the equivalent of the CComEnum’s A tl FI agNoCopy flag. While this is more efficient than A tl FI agCopy or the manual copying required for A tl FI agTakeOwnershi p, if the collection changes while it’s being enumerated, the behavior is undefined. If you need ATL’s 326 ATL INTERNALS STL-based enumerator to have its own copy of the data, y o u ,ll have to wrap a copy of the data in its own COM object, a technique I’ll show you later. CComEnumOnSTL Usage If our prime number collection object held an STL collection of VARIANTS, the implementation of get_ NewEnum would look like this: STDMETHODIMP CPrimeNumbers::get—NewEnum(IUnknown** ppunkEnum) { *ppunkEnum =* 0; typedef CConEnunOnSTL, vector > CCowEnuiRVari antOnVector; CCoaObject^ pe = 0; HRESULT hr * CComObj ect::Createlnstance(Ape); if( SUCCEEDED(hr) ) { pe->AddRef(); hr = pe->Init(this«>CetUnknoMnC), n„rgPrimes); if( SUCCEEDED(hr) ) { hr = pe->QueryInterface(ppunkEnum); } pe->Release(); > return hr; } Of course, we’d prefer not to keep a collection of VARIANTS. Instead, we’d like to keep a collection of a type that matches our needs, in this case, 1 ongs. Fortu­ nately, unlike CComEnumlmpl, IEnumOnSTLImpl allows on-demand data conver­ sion, allowing us to keep our collection in a convenient type but still providing the data in a format required by the enumerator. On-Demand Data Conversion The implementations of the Next, Ski p, Reset, and Cl one methods using an STL collection are almost identical to those of the CComEnumlmpl class. The single significant difference is a nifty loophole in the IEnumOnSTLImpl *s Next method. The CComEnumlmpl class ties the data type being enumerated to the data type held COLLECTIONS AND ENUMERATORS 327 in the array of the enumerator. However, IEnumOnSTLImpl has no such limitation. Look at this snippet from IEnumOnSTLImpl’s Next method: template ::Next(ULONC celt, T*rgelt, ULONC* pcelt Fetched) { • • • T* pelt ■ rgelt; while (SUCCEEDED(hr) && waiter !■ m_pcollection->end() && nActual < celt) { hr * Copy:: copy (pelt, return hr; M The template parameters allow the type of the *pel t to be different from the type of the &*nui te r. In other words, the type of data that the collection holds can be different from the type of data that the client receives in the call to Next. This means that the copy policy class must still be able to initialize and destroy the data of the type being enumerated, but the copy operation could actually be hyacked to con­ vert from one data type to another. Imagine the following copy policy: struct _CopyVar1antFromLong { s ta tic HRESULT copy(VARIANT* p i, long* p2) { pl->vt » VTJL4; pl->1Val = *p2; r#tum S.OK; > static void init(VARIANT* p) { Variantlnlt(p); } static void destroyCVARIANT* p) { VariantClear(p); } If the STL collection held longs, but the enumerator expoaed VARIANTS, the —CopyVari antFromLong copy policy could be used to convert that data on de- mand. For example, if the prime number collection object was keeping an STL AT l INTERNALS collection of 1 ongs, the following code would create an enumerator that could con­ vert from long to VARIANT as appropriate during the client’s Next call: STDMETHODIMP CPrimeNumbers::get—NewEnum(IUnknown** ppunkEnum) { *ppunkEnum = 0; typedef CCoinEnumOnSTL > CComEnumVariantOnVectorOfl ongs; CComObject * pe = 0; … // The rest is the same! The only difference between this example and the previous one is the enumerator type definition. Instead of building it using a vector of VARIANTS, we build it using a vector of longs. Since the data type of the collection is different from the data type of the enumerator, we simply provide a copy policy class whose copy method converts appropriately. This is an especially useful technique for mapping between whatever is the most convenient type to hold in your collection object and VARI­ ANTS to support the VB For-Each syntax. Giving CComEnumOnSTL Its Own Copy As I mentioned, unlike CComEnum, CComEnumOnSTL doesn’t provide an option to copy the data held by the collection. Instead, it assumes it will share the data with the collection. Sometimes this can lead to undefined behavior if the collection is be­ ing modified while it is also being enumerated. All is not lost, however. It is possible to give a CComEnumOnSTL object its own copy of the data. The key is to build a COM object whose job it is to hold the STL container for the life of the enumerator. Then, when I n i t is called, pUnkForRel ease is the pointer to this container copy object. Once the enumerator is done, it will release the container copy object, thus de­ stroying the copy of the data. Unfortunately, ATL provides no such class. Fortu­ nately, it’s easy to build one. CComContai nerCopy is a generic class for holding a copy of aii STL container. The complete implementation follows: template class CComContai nerCopy : public CComObjectRootEx, P'jhlic IUnknown { // CComEnumOnSTL only needs an IUnknown* COLLECTIONS AND ENUMERATORS 329 public: HRESULT Copy(const CollType* coll) { try { m_col1 = col1; return S_OK; > catchC...) { return E^OUTOFMEMORY; BECIN_COM_MAP(CComContainerCopy) COM_INTERFACE_ENTRY(IUnknown) END_COM_MAP() CollType m一 coll; Notice that the CComContai nerCopy class is parameterized by the type of col­ lection it is to hold. This class can be used to copy any STL-like container. The Copy method copies the collection using assignment. Because the CComContai nerCopy class derives only from IUnknown, it is ideally suited for one purpose: as the first argument to IEnumOnStl Im pls In i t method. The second argument is the public m_col 1 member. Using the Copy method of the CComContai nerCopy class mim­ ics the use of the CComEnum class’s A tl FI agCopy. The collection already has the data in the appropriate format,but the enumerator should have its own copy. Popu­ lating the m一 col 1 member of the CComContai nerCopy directly is like A tl FI ag­ TakeOwnershi p. The collection doesn’t already have the data in the appropriate format, but the container has converted the data for use by the enumerator. An ex­ ample usage of CComContai nerCopy using the Copy method follows: STDMETHODIMP CPrimeNumbers::get__NewEnum(IUnknown** ppunkEnum) { ☆ppunkEnum = 0; typedef CComEnumOnSTL, vector > CComEnumVariantOnVector; CComObject* pe = 0; HRESULT hr - CComObject::CreatelnstanceC&pe); 330 ATL INTERNALS if( SUCCEEDED(hr) ) { pe->AddRef(); // Create the container copy CComObject< CCo«iiContainerCopy< vector > >* pCopy = 0; II Use pCopy as a scoping mechanism to bind to the static // Createlnstance hr = pCopy->CreateInstance(&pCopy); if( SUCCEEDED(hr) ) { pCopy->AddRef(); // Copy the STL container to the container copy hr = pCopy->Copy(m_rgPrimes); if( SUCCEED印 (hr) ) { // Init the enumerator with the copy hr = pe->Init(pCopy->Cetllnknown() , pCopy->m_coll); if( SUCCEEDED(hr) ) { hr pe->QueryInterface(ppunkEnum); } } pCopy->Release(); } pe->Release(); } return hr; On-Demand Data Calculation CComEnum requires initialization with an array of data that is already calculated. CComEnumOnSTL, on the other hand, accesses the data by calling member functions on objects we provide. Therefore, calculating data on demand is a matter of pro­ viding implementations of the member functions that perform the calculations in­ stead of accessing precalculated results. For example, there’s no reason the collection of prime numbers needs to pre­ calculate all the results and store them. Instead, we need an STL-like container that looks like what CComEnumOnSTL needs (as I showed you before) but calculates the next prime number on demand. This container has two responsibilities. The first is keeping track of the range of values to iterate over. The second responsibility is COLLECTIONS AND ENUMERATORS 331 exposing an iterator for both the beginning and one past the ending of the data. The beginning and ending iterator must be exposed via b eg i n and end methods, and each must return a value of type ite r a to r , a type nested inside the class. The PrimesContai ner class lives up to both these responsibilities: class PrimesContainer { public: class iterator; // Forward declaration PrimesContainer() : m_min(0) , m_max(0 ) {} II For IPrimeNumbers::CalcPrimes void SetRange(long min, long max) { numin = min; m一max = max; } // For IPrimeNumbers::get_Count size_t sizeO { return CountPrimes(m_min, m一 max); } // For IPrimeNumbers::get一 Item long operator[](size_t i) { return NthPrime(i + 1, m一 max); } // The rest is for CComEnumOnSTL iterator begin() { return iteratorm_max); } iterator end() { return iterator(); } class iterator private: 1 ong m_ini n, m_max; Notice that in addition to supporting the minimum interface as required by the implementation of CComEnumOnSTL, the P rim esC ontai ne r class also provides a SetRange method for managing the range of prime numbers, a s ize method for countir\g the prime numbers in the range, and an o p e ra to r [] method for extract­ ing items in a random-access fashion. These methods make the Pri mesContai ne r class suitable for implementing the IPrimeNumbers interface. 332 ATL INTERNALS class ATL_NO_VTABLE CPrimeNumbers : public CComObjectRootEx, public CComCoClass, public IDispatchlmpl { public: // IPrimeNumbers public: STDMETHODIMP CalcPrimes(long min, long max) { m_rgPrimes.SetRange(min, max); return S一 OK; } STDMETHODIMP get_Count(long* pnCount) { *pnCount = m_rgPrimes.size(); return S_OK; } STDMETHODIMP get_Item(1ong n, long* pnPrime) { if( n < 1 || n > m^rgPrimes.size() ) return E—INVALIDARG; *pnPrime = m_rgPrimes[n-l]; return S一OK; } STDMETHODIMP get 一 NewEnum(IUnknown** ppunkEnum) { ☆ppunkEnum = 0; typedef CCofflEnumOnSTL CComEnumVari antOnPriinesContai ner; CComObject* pe = 0; HRESULT hr = pe->CreateInstanceC&pe); if( SUCCEEDED(hr) ) { pe->AddRef(); hr = pe->Init(this->GetUnknown() , m 一 rgPrimes); if( SUCCEEDED(hr) ) { hr = pG->QuGryInterface(ppunkEnum); } pe->Release(); } return hr; } COLLECTIONS AND ENUMERATORS private: PrimesContainer m_rgPrimes; In fact, this code is nearly identical to the code I ve already shown you. The differ­ ence is that instead of using a container that already has a precalculated set of val­ ues, we have one that knows how to calculate thcni on demand. Specifically, it’s the iterator that does the magic: class PrimesContainer { iterator begin() { return iterator(m 一 min, m_max); } iterator end() { return iterator(); } class iterator { public: iterator (long min = -1, long max = -1) : m 一 max(max), m_next(NthPrimeCI, min, max)) { if( m一 next == -1 ) m .max = -1; } // Match end() bool operator!=(const iterator& rhs) { return (m一 next != rhs.m—next I| m—max != rhs.m一max); } long& operator*。 { return m—next; } iterator operator++(int) { iterator it(m_next, m_max); m—next «= NthPrime(l, m_next + 1 , m—max); "if( m_next == -1 ) m_max = -1; // Match end() return it; > private: 334 ATL INTERNALS The key to understanding the iterator is understanding how CComEnumOnSTL uses it. CComEnumOnSTL keeps a pointer to the collection, called m一 p co l 1 e c t i on, and an iterator, called m_i te r, that marks the current position in the container. The m—ite r data member is initialized when the enumerator is constructed or when Reset is called to the result of nupcol 1 e c ti on->begi n (). The implementation of begin constructs an iterator that uses the range of possible prime numbers to cache the next prime number and the maximum number to check. As the container is iterated, the next prime number is calculated one ahead of the request. For every element in the container, the following sequence is performed: 1. m_pcol 1 ec ti on_>end () constructs an iterator that marks the end of the data. This, in turn, creates an iterator with -1 for each of m_mi n, irumax, and m一 next. Special member data values are common for constructing an iterator that marks the end of the data. 2. o p e rato r! = compares the current iterator with the ending iterator. 3. operator* pulls out the prime number at the current location of the iterator. 4. The postfix operator++ calculates the next prime number. If there are no more prime numbers, m_mi n, m_max, and m _next are each set to _1 to indicate the end of the data. The next time through the loop, the comparison with the ending iterator will succeed and CComEnumOnSTL will detect that it has reached the end of the collection. This behavior can be seen by looking at the main loop in the CComEnumOn- STLImpl:: Next implementation: template STOMETHOOIMP IEnumOnSTtlmpl end() 8A nActual < celt) { COLLECTIONS AND ENUMERATORS 335 hr » Copy:: copy (pelt, 4 * 0 t€r); if (FAILED(hr)) { while (rgelt < pelt) Copy::destroy(rgelt++); nActual ■ 0; } else { nActual++; } > • • • return hr; If you find occasion to calculate data on demand using a custom container and it­ erator pair, yours will be called in the same sequence. This gives you an opportunity to calculate data appropriately for your data set, for example, lines in a file,records in a database, bytes from a socket Why go to all this trouble to calculate data on de­ mand? Efficiency in both time and space. There are 9,592 prime numbers between 0 and 100,000. Precalculating and storing the primes as longs costs nearly 38K. Worse, the client must wait for all primes to be calculated in this range even if it never gets around to enumerating them all. On the other hand, calculating them on demand requires the m_jni n and m_jnax members of the container and the rrunext and m_max members of the current iterator. That’s 16 bytes no matter how many prime numbers we’d like to calculate, and the cost of calculating them is only real­ ized when the client requests the next chunk.6 Collections ICollectionOnSTLImpI In addition to parameterized implementations of enumeration interfaces, ATL pro­ vides parameterized implementations of collection interfaces, assuming you’re will­ ing to keep your data in an STL-llke container. The implementation is provided by the ICollectionOnSTLImpI class: 6Of course, there are far more efficient ways to store and calculate prime numbers than what I have shown here. Even so, there are going to be space vs. time tradeoffs that make calculating on demand an attractive option. ATL INTERNALS template class ICol1ectionOnSTLI«pl : public T { public: STDMETHOO(get_Count)(long* pcount); STDMETHOO(get_Item)(long Index, ItemType* pvar); STDMETHOD(get—NewEnum)(IUnknown** ppunk); CollType m_col1; The IC ol 1 e c ti onOnSTLImpl class provides an implementation of the three stan­ dard collection properties very much like what I showed you earlier. The chief dif­ ference is that the STL container is managed for you in the m_col 1 member data of the IC ol 1 e c ti onOnSTLImpl class. That means that you can’t provide a copy of the data to the enumerators, but you can still use a collection that calculates on demand and you can still convert from a convenient type to the type required by the enu­ merator exposed from get_ NewEnum. This is because, although you get to decide the type of the container in a template parameter, you’re no longer implementing g e t 一 NewEnum. The template parameters of IC ol 1 e c ti onOnSTLImpl are as follows: ■ The T parameter indicates the base class, for example, IDi spatchlmpl . ICol 1 e cti onOnSTLImpl will provide the implementation of the standard three properties of this base class, but the deriving class is responsible for the rest • The C o ll Type parameter indicates the type of STL-like container to keep, for example, vector orPrimesContainer. ■ The ItemType parameter indicates the type of data exposed from the iterator of the collection, for example, 1 ong. ■ The Copyltem parameter indicates the type of the copy policy class. This copy policy will be used only in the implementation of the g e t_ Ite m method. The copy policy should be able to copy from a container holding items of type ItemType to a single [out] parameter of type ItemType. If you were manag­ ing a container of 1 ong numbers, the Copyltem type would be _Copy. ■ The EnumType parameter indicates the type of the enumeration implemen­ tation class. This enumeration must be able to enumerate over an STL-like container just like CComEnumOnSTL. An example EnumType parameter is CComEnumOnSTLImpl, vector >. C C)! i f C r I O N S AND ENUMERATORS ICollectionOnSTLImpI Usage The best way to understand the ICollectionOnSTLImpI class is to see it in ac­ tion. The first STI^based implementation of the IPrimesCollection standard collection interface a.ssim\c*d we wanted to manage a precalculated container of VARIANTS. This could he done using ICol 1 ectionOnSTLImpl like so: // Needed for implementation of get. Ttem. // Converts the storage type (VARIANT) to the item type (long) struct ^CopyLonqFromVariant { static HRESULT copy(1ong* pi, VARIANT* p2) { i f( p2->vt == VT_I4 ) { -pi - p2-->1Va 1 , return b_OK; } else { VARIANT var; HRESUL1 hr = VariantChangeType(&var, p2, 0, VI_14); if( SUCCEEDED(hr) ) -pi = var.lVal; return hr; } static void ini t(long* p) { } static void destroy(long* p) { } // Needed for implementation of IDispatch methods typedef IDi spatchlmpl IPr imeNumbersDualImpl; // Needed for i mpl emen* a r i of get.. ^ewLniim mer hod typedef CComEnumOnSll , v*;ctor > CComEriumVar iafUOnVector; II Needed for implernentarion of SLandard collection methods typedef ICollection0nS7Hmpl, long, _CopyLongFrontVariant, CComEnumVariantOnVector> IPrimeNumbersColllmpl; 338 AIL INTERNALS class ATL_NO_VTABLE CPrimeNumbers : public CComObjectRootEx, public CComCoClass, public IPrimeNumbersCol1Impl { public: • • • II IPrimeNumbers public: STDMETHODIMP CalcPrimes(1ong min, long max) { m_col1 .clearC); for( long n = min; n <= max; ++n ) { if( IsPrime(n) ) { VARIANT var = {VT_I4}; var.lVal = n; m_coll.push_back(var); > } return S一 OK; If we’d like to precalculate the prime numbers but keep them as a vector of 1 ong numbers, this is how we’d use ICol 1 ectionOnSTLImpl: // Needed for implementation of get—NewEnum. // Converts the storage type (long) to the enumeration type (VARIANT), struct _CopyVariantFromLong { static HRF.5ULT copy(VARIANT* pi, long* p2) { pl->vt = VT_I4; pl->lVal = *p2; return S_0K; } else { VARIANT var; HRESULT hr = VariantChangeType(&var, p2, 0, VT 一 14); if( SUCCEEDED(hr) ) *pl = var.lVal; return hr; COLLECTIONS AND ENUMERATORS static void init(long* p) { } static void destroy(long* p) { } }; // Needed for implementation of IDispatch methods typedef IOispatchlmpl IPrSmeNumbersDuallmpI; // Needed for implementation of get_ NewEnum method typedef CComEnumOnSTL > CComEnumVariantOnVectorOflongs; // Needed for implementation of standard collection methods typedef ICol1ectionOnSTLImpl, long, _Copy, CCowEnumVari antOnVectorOf1ongs> IPrimeNumbersCoTlImpI; class ATL_NO_VTABLE CPrimeNumbers : public CComObjectRootEx, public CComCoClass, public IPrimeNumbersColUmpl { public: // IPrimeNumbers public: STDMETHODIMP CalcPrimes(long min, long max) { m_col 1 .clearO; for( long n = min; n <=: max; ++n ) { 1f( IsPrime(n) ) { w»col1•push—back(n); > } return S_OK; 340 INTERNALS :Inally. if we*d like to have the prime numbers calculated on demand and ex- }><-srvi a,s iong numbers, wed use ICol 1 ectionOnSTLImpl like so: // C«Icalates prime numbers on demand class PrimesContainer, // Needed for imp :e.nenration of get_Item. // Converts the sloraye type (VARIANT) to the item type (long). r'. auh*r ; // Needed fo;. imple^n-itation of IDispatch methods typedef ID' spatchlmp I IPrimeNumbersDua'IImpl ; // Needed for implementation of get_ NewEnum method typedef CComEnumOnSTL CComEnumVari antOnPrimesContainer; II Needed for implementation of standard collection methods typedef ICol 1 ectionOnSTHmpl , CComEnumVariantOnPrimesContainer> > IFrimeNumoersCollImpl; c. lass ATL._N0..VTA8Lf: CPrimeNumbers : pub'ic CComObjectRootEx, pub i it: CComCoClass, public lFt vneNunbersCollImp") { pdb1iC: • • • II lPrim«il>iijmbers publ 丨 c: STDMETHODIMP Calc.Primes(long min, long max) { m_coll.SetRangeCmin, max); } Jim Springfield, the Father of ATL, says “ICol 1 ec ti onOnSTLImpl is not for the faint of heart.” He's absolutely right. It provides a lot of flexibility, but at the COLLECTIONS AND ENUMERATORS expense of complexity. Still, once you’ve mastered the complexity, as with any good class library, you can get a lot done with very little code. STL Collections of ATL Data Types If you’re an STL fan (many modem developers are), you may find yourself wishing to keep some of ATL’s smart types (that is, CComBSTR, CComVariant, CComPtr, and CComQIPt r) in an STL container. Many STL containers have a requirement con­ cerning the elements they hold that makes this difficult for STL smart types: ope r- ator& must return an address to an instance of the type being held. However, all the smart types except CComVa ri an t overload ope ratorA to return the address of the internal data: BSTR* CComBSTR:operator&() { return 4m_str; } T** CComPtr::operator&C) { ATLASSERT(p««NULL); return &p; > T** CComQIPtr::operator&() { ATLASSERT(p**NULL); return &p; } These overloads mean that CComBSTR, CComPt r, and CComQI Pt r cannot be used in many STL containers or with STL algorithms with the same requirement. The clas­ sic work-around for this problem is to maintain a container of a type that holds the ATL smart type but that doesn’t overload operator&. ATL provides the CAdapt class for this purpose. ATL Smart-Type Adapter The CAdapt class is provided for the sole purpose df w r^ping ATL smart types for use in STL collections. It’s parameterized to accept any of the current or future such types: template class CAdapt { publ彳 c: CAdapt() {} CAdapt(const T& rSrc) { mJT - rSrc; } CAdapt(const CAdapt^ rSrCA) { • rSrCA.m_T; } CAdaptA operator-(const T& rSrc) { mJT ■ rSrc; return *this; } 342 ATL INTERNALS bool operator<(const T& rSrc) const { return mJT < rSrc; } bool operator»»(const T& rSrc) const { return mJT rSrc; } operator TA() { return m_T; > operator const TA() const { return »n_T; > T m>T: Notice that CAdapt does not have an operator&, so it will work just fine for STL containers and collections. Also notice that the real data is held in a public member variable called m_T. Typical usage requires using either this data member or a s ta ti c_cast to obtain the underlying data. CAdapt Usage For example, imagine that you wanted to expose prime numbers as words instead of digits. Of course, you’d like the collection to support multiple languages, so you’d like to expose the strings in Unicode. Also, you’d like to support type-challenged COM mappings, so the strings have to be BSTRs. These requirements suggest the fol­ lowing interface: [dual] interface IPrlneNumberWords : IDispatch { HRESULT CalcPrimes([in] long min, [in] long max); [propget] HRESULT Count([out, retval] long* pnCount); [propget, id(DISPID_VALUE)] HRESULT Item([in] long n, [out, retval] BSTR* pbstrPrincWord); [propget, id(DISPID_NEWENUM)] HRESULT —NewEnum([out, retval] IUnknown** ppunkEnum); COLLECTIONS AND ENJMERATORS Notice that the Item property oxptjses the prime number as a string, not a miinber. Also keep in mind that although the signature ()f 一 NewEnum is unchangtHl. wt、will be returning VARIANTS to the client that contain BSTRs, not long numbers. Because we’re dealing with one of the COM data types that’s inconvenient for ( ’+ 十 progian⑴ lers. BSTRs, we'd like to use the CComBSTR smart data type de­ scribed in Chapter 2. However, the compiler will coinplain if we try to use a data member like this to maintain the data: vector m_rgPrimes; Instead, we’ll use CAdapt to hold the data: vector< CAdapt > m_rgPrimes; Of course, because we re using strings, our method impleinentations change. To calculate the data, we’ll be changing the prime numbers to strings: STDMETHODIMP CPrimeNumberWords::CalcPrimes(long min, long max) { whi1e( min <* max ) { if( IsPrime(min) ) { char sz[64]; CComBSTR bstr = NumWordCmin, sz); m_rgPrimes•push 一 back(bstr); return S一 OK; Notice how we can simply push a CComBSTR onto the vector. The compiler will uso the CAdapt constructor that takes a const CComBSTRA to con­ struct the appropriate object for the vector to manage. The get 一 Count method doesn't change, but the get一 I tem method does: STPMtTMODIMP CPrimeNumberWords::get_Item(long n, BSTR* pbstrPrimeWord){ if( n < 1 1| n > m_rgPrimes.size() ) return E一 INVAL1DARC; CComBSTR& bstr = m_rgPrimes[n-l].m_T; return bstr.CopyTo(pbstrPrimeWord); ATL INTERNALS Notice that we're reaching into the vector and pulling out the appropriate element. Again, remember that the type of element we're holding is CAdapt , so I’ve used the m_T element to access the CComBSTR data inside. However, because the CAdapt class has an implicit cast operator to CComBSTR&. using the m一T member explicitly is not necessary. Finally, the get_ NewEnum method must also change. Remember that we’re implementing IEnumVARIANT,but instead of holding long numbers, we’re holding BSTRs. Therefore, the on-demand data conversion must convert between a CAdapt 〈CComBSTR〉(the data type held in the container) to a VARIANT holding a BSTR. This can be accomplished with another c ustom copy policy class: struct 一 CopyVariantFromAdaptBstr static HRESULT copy(VARIANT* pl->vt = VT一BSTR; pl->bstrVal = p2->m_T.Copy() return Cpl->bstrVal ? S_OK •• > { pi, CAdapt* p2) { E_OUTOFMEMORY); static void init(VARIANT* p) { Variantlnit(p); static void destroy(VARIANT* p) { VariantClear(p) The corresponding enumeration type definition looks like this: typedef CComEnumOnSTL > > CComEnumVariantOnVectorOfAdaptBstr; Using these two type definitions, implementing get_ NewEnum looks much like it always does: STDMETHODIMP CPrimeNumberWords::get—NewEnumCIUnknown*^ ppunkEnum) { *ppunkEnum - 0 ; CCowObject^ComEnumVariantOnVectorOfAdaptBst^* pe = 0; HRESULT hr = pe->CreateInstance(&pe); if( SUCCEEDED(hr) ) { pe->AddRef(); COLLECTIONS AND ENUMERATORS 345 hr = pe->Init(this->CetUnknown(), nurgPrimes); if( SUCCEEDED(hr) ) { hr = pe->QueryInterface(ppunkEnufr); pe->Release(); return hr; Using ICollectionOnSTLImpI with CAdapt If you'd like to combine the use of ICol 1 ectionOnSTLImpl with CAdapt, you al­ ready have half the tools, nainely, the custom copy policy and the enumeration type definition. You still need another custom copy policy that copies from the vector of CAdapt to the BSTR* provided by the client to iniplement get_Item. This copy policy can be implemented like so: struct _CopyBstrFromAdaptBstr { static HRESULT copy(BSTR* pi, CAdapt* p2) { *pl = SysAllocStri ng(p2->m_T); return (*pl ? S一OK : E^OUTOFMEMORY); } static void init(BSTR* p) { } static void destroyCBSTR* p) { SysFreeString(*p); } }; Finally, we can use CAdapt with ICollectionOnSTLImpI like so: typedef IDispatchlmpl IPrimeNumberWordsDualImpl; typedef ICol1ectionOnSTLImpl〈IPrimeNumberWordsDualImpl, vector< CAdapt > , BSTR, _CopyBstrFromAdaptBstr, CComEnumVariantOnVectorOfAdaptBstr> IPrimeNumberWordsCol1Impl; ATL INTERNALS class ATL_NO_VTABLE CPrimeNumberWords : publ i c CComObjectRootEx, public CComCoClass, public IPri meNumberWordsCol1Impl { public: // IPrimeNunberWords public: STDMETHODIMP CalcPrimes(long min, long max) { while( min <- irax ) { if( IsPrime(min) ) { char sz[64]; CComBSTR bstr = NumWord(jrii n, sz); m 一 col 1.push_back(bstr); } return S_OK; Simple Collections Using STL puts one burden firmly on the shoulders of the developer: exception handing. Many calls into STL collections and algorithms can cause exceptions that must be caught before they leave the method boundary.7 And because C++ ex­ ception handling requires the C runtime (CRT), the CRT libraries must be linked with any ATL project that uses STL. Unfortunately, many ATL servers are built with­ out the CRT, and therefore a replacement for the STL is needed. ATL provides two simple classes that provide basic array and map functionality that are not unlike the STL vector and map classes. In the spirit of ATL, neither of these classes throws exceptions or requires the CRT. T lotting a C + + or Win32 structured exception escape a COM method is illegal. All such exceptions must b<* caught and turned into appropriate HRESULTs. For more information on this topic, see Effective COM, by Don Box, Keith Brown, Tim Ewald, and Chris Sells (Addison-Wesley, 1998). COLLECTIONS AND ENUMERATORS 347 CSimpleArray The simple array class in ATL is named, appropriately enough, CSimpleArray. It’s a dynamically sized array that grows on demand. It is a template class, so it can hold any kind of data. Its declaration is as follows: template class CSiaplcArray { publi c: T* m-aT; 1nt nunSlze; 1nt BL-nAllocSize; // Construet1on/d«struction CS1wp1«Array( > ; -CSi«ipleArrayO; // Operations in t C€tS1zeC) const; BOOL Add(T« t); BOOL RcfBoveCTdr t ) ; BOCH. ReaoveAt(1nt nlndex); void RemoveAl1(); T* operator[] (int nlndex) const; T* C«tOata() const; void SetAtInd€x(1nt nlndex. T4 t); In t F1nd(TI» t ) const; The class members manage the memory associated with the rruaT data mem­ ber, a dynamically sized array of type T. Unfortunately, CSi mpl eArray isn’t too use­ ful for implementing an enumeration interface, even though it could be easily used with CComEnum, because you’re not likely to want to hold data in the same type as is being enumerated. Because CComEnum doesn’t support conversion on demand like CComEnumOnSTL does, you must manually convert your CSi mpl eArray data into an array of data appropriate for enumeration. However, that doesn’t mean that CSi mpl eAr ray can’t be used for simpler (sic) jobs. CSimpleValArray Notice that in CSimpleArray, we’re passing around references to data elements. This works fine for user-defined types, but not so well for simple types that have constant values; that is, the compiler frowns on passing 4 by reference. For these 348 ATL INTERNALS simple types, ATL provides CSimpleValArray, where arguments are passed by value instead: template class CSi«pleValArray : public CSimpleArray { public: BOOL AddCT t) { return CSimpleArray::Add(t); } BOOL Remove(T t) { return CSimpleArray::Remove(t); } T operator〔] (int nlndex) const { return CSimpleArray::operator[](nlndex); } CSimpleMap If you’d like the functionality of the STL map class without the burden of the CRT, ATL provides CSi mpl eMap: template class CSiapleMap { public: TKey* m.aKey; TVa” AL.aVal; int nunSi ze ; // Construction/destruction CSimpleMapO; -CSimpleMap(); II Operations int CetSizeO const; BOOL Add(TKey key, TVal val); BOOL Remove(TKey key); void RemoveAl1(); BOOL SetAt(TKey key, TVal val); TVal Lookup(TKey key) const; TKey ReverseLookup(TVal val) const; TKeyA CetKeyAtCint nlndex) const; T V a U GetValu€At(int nlndex) const; void SetAtIndex(int nlndex, TKeyA key, TValA val); int FindKey(TKey& key) const; COLLECTIONS AND ENUMERATORS 349 int FindVal(TValA val) const; CSimpl eMap maintains two matching dynamically sized arrays. Each element in the key array matches an element of the value array. CSimpl eMap would be useful for implementing collection item lookup by name instead of by index. Object Models A COM object model is a hierarchy of objects. Collections allow the subobjects to be manipulated. Enumerators allow these objects to be accessed. Most object mod­ els have one top-level object and several noncreateable subobjects. The following stylized IDL shows a minimal object model: library OBJECTMODELLib { impo 「tlib("stdole32.tlb"); import1ib("stdole2.tlb"); // Document subobject //////////////////////////////////////// [ dual ] interface IDocument : IDispatch { [propget] HRESULT Data([out, retval] BSTR ^pVal); [propput] HRESULT Data([in] BSTR newVal); coclass Document { [default] interface IDocument; // Documents collection /////////////////////////////////////// [ dual ] 彳的6 # 3 〔6 IDocuments : IDispatch { HRESULT AddDocument([out, retval] IDocument** ppDocument); [propget] HRESULT Count([out, retval] long* pnCount); [id(DISPID_VALUE), propget] HRESULT Item([in] long n, [out, retval] IDocument** ppdoc); [id(OISPID_NEWENUM), propget] HRESULT _NewEnum([out, retval] IUnknown** ppEnum); coclass Documents { [default] interface IDocuments; 350 ATL INTERNALS Figure 7.3. Simple object model instance hierarchy // Application top-level object //////f///////f//////i///////// [ dual ] interface IApplication : IDispatch { [propget] HRESULT Documents([out, retval] IDocuments** pVal); }; coclass Application { [default] interface IApplication; An instance hierarchy of this object model would look like Figure 7.3. Implementing the Top-Level Object The top*level object of an object model is createable and will expose any number of properties as well as any number of collection subobjects. The example implemen­ tation looks like the following: class ATL_N0_VTABLE CApplication : public CComObjectRootEx, public CComCoClassf public IDi spatchlmpl { public: DECLARE_RECISTRY^RESOURCEIO(IDRJ\PPtICATION) OECLARE_NOTJ^CGRECATABLE(CApplication) DECLARkPROTECT_FINAL_CONSTRUCT() BEGIN—COM—MAP(CAppli cation) COM一 INTERFACE—ENTRY(IApplication) COM一 INTERFACE—ENTRY(IDi spatch) END_C0M_MAP() COLLECTIONS AND ENUMERATORS // Create instance of the Documents collection HRESULT (Application::FinalConstruct() { return CDocuraents::Createlnstance(&m_spOocuments); } I I IApplication public: // Hand out the Documents collection to interested parties STDMETHODIMP CApplication::get_Documents(IDocuments** pVal) { return m_spDocuments.CopyTo(pVal); } CComPtr m_spDocuments; Implementing the Collection Object The colloct ion object is the most difficult of the three layers to implement, not be­ cause of any difficult code but because of the maze of type definitions. The first set is required to implement the enumerator: template struct -CopyVariantFromAdaptltf { static HRESULT copy(VARIANT* pi, CAdapt< CComPtr >* p2) { HRESULT hr = p2->m_T->QueryInterface(IID_TDispatch, (void**)&pl- >pdi spVal); ifC SUCCEEDED(hr) ) { pl->vt = \TT_DISPATCH; } else { hr = p2->m_T->QueryInterface(IID_IUnknown, (void**)&pl->punkVa1); if( SUCCEEDEO(hr) ) { pl->vt = VT_UNKNOWN; static void init(VARIANT* p) { Variantlnit(p); } static void destroy(VARIANT* p) { VariantClear(p); } ATL INTERNALS typedef CCofnEnumOnSTL, 1ist< CAdapt< CComPtr > > > CComEnumVariantOnListOfDocuments; The -CopyVari antFromAdaptltf class is a reusable class that converts an inter­ face into a VARIANT for use in enumerating a collection of interface pointers. The collection object is expected to hold an STL container of elements of type CAdapt struct _CopyItfFro»AdaptItf { static HRESULT copy(T** pi, CAdapt< CComPtr >* p?) { ifC *pl = p2->m_T ) return (*pl)->AddRef(), S一OK; return E.POINTER; } static void init(T** p) {} static void destroy(T** p) { if( *p ) (*p)->Release(); } typedef ICol1ecti onOnSTLImpl< IDi spatchlmpl, list< CAdapt< CComPtr > >, IDocument*, _CopyItfFromAdaptItf, CComEnumVariantOnlistOfDocuments> IDocumentsCol1Impl; The _C opyItfF rom A daptltf is used to implement the Item property, again as­ suming an STL container holding elements of type CAdapt >. The copy policy is then used to define the collection interface implementation, IDocu­ mentsCol 1 Impl. Finally, IDocumentsCol 1 Impl is used as the base class of the IDocuments implementation: C 0 1 1.1. C T ! o N S AND ENUMERA1 ORS class ATL_NCLVTABLE CDocuments : public CComObjectRootfx, public CComCoClass, // noncreateabl e public IDocumentsCol1 Imp! { public: DECLARE_NO_REGISTRY() DtCLARC.NOT^ACGRECATABLE(CDocuments) DECLARE_PROTFCT_FINAL_CONSTRUCT() BLGIN^COM„MAP(CDocuments) COM. TNTERFACE_ENrRY(lDocuments) COM. INTERFACE_ENTRV(IDispatch) END_COM_MAP() // IDocuments public: STDMETHODIMP AddDocument(IDocument** ppOocument) { // Create a document to hand back to the client HRESULT hr = CDocument::CreateInstance(ppDocument); if( SUCCEEOED(hr) ) { // Put the document on the 1ist CComPtr spDoc = •ppDocument; m_col1.push_back(spDoc); > return hr; Th(、benefit of all the type dt'finilions is tlial the standard methods of the col­ lodion are implemeiuod tot us. WV have but to linplement the AddDocument method, which creai(\s a n(iw CDocument and adds it to the list maintained by the IColl ecti onOnSTLImpl class. Implementing the Subobjects The suhohjecls can Ho whatever you'd like, inc luding maintaining collections of objecLs further down the hiorarrhy. Our example maintains a BSTR. representing its data: ATL INTERNALS STDMETHODIMP CDocument::get—Data(BSTR *pVal) { return m一 bstrData.CopyTo(pVal); STDMETHODIMP CDocument::put_Data(BSTR newVal) { m 一 bstrData = newVal; return (nubstrData |I !newVal ? S_OK : E_OUTOFMEMORY); } Using the Object Model You normally design an object model to be used by many language mappings, in­ cluding scripting environments. Here’s an example HTML page that uses this ex- ainple object model: Summary COM has abstractions much like those of STL. Collections maintain lists of things, often objects. Enumerators enable navigation over the list of things maintained in a collection. To standardize access to collections and enumerators, they have a stan- COLLECTIONS AND ENUMERATORS dard protocol. These standards aren’t required, but if they are followed, they make an object model programmer's life easier because the usage will be familiar. Imple­ menting an object model is a matter of defining the higher-level object, the level- level object and the collection that joins the two together. ATL will implement both collection and enumeration interfaces, if you’re not afraid of the type definitions re­ quired to make it all work. CHAPTER 8 Connection Points A Review of Connection Points An object implements one or more interfaces to expose its functionality. The term connection p o in ts refers to a logically inverse mechanism that allows an object to expose its capability to call one or more specific interfaces. Another perspective is that Querylnterface allows a client to retrieve from an object a pointer to an interface that the object implements. Connection points al­ low a client to give an object a pointer to an interface that the client implements. In the first case, the client uses the retrieved interface pointer to call methods pro­ vided by the object In the second case, the object uses the provided interface pointer to call methods provided by the client. A slightly closer inspection of the two mechanisms reveals that Q uerylnter­ face allows a client to retrieve from an object only those interfaces that the client knows how to call. Connection points allow a client to provide to an object only those interfaces that the object knows how to call. A connection has two parts: the object making calls to the methods of a specific interface, called the source or, alternatively, the connection p o in t; and the object implementing the interface (receiving the calls), called the sink object (Figure 8.1). Using my terminology from the prior paragraphs, the object is the source and makes calls to the sink interface methods. The client is the sink and implements the sink interface. One additional complexity is that a particular source object may have connections to multiple sink objects. The IConnectionPoInt Interface A client uses the source object’s implementation of the IC onnecti onPoi n t inter­ face to establish a connection. Here is the definition of the IConnecti onPoi nt interface: interface IConnecti onPoi nt : IUnknown HRESULT CetConnectionlnterface ([out] IID* pIID); HRESULT CetConnectionPointContainer ([out] IConnectionPointContainer** ppCPC); 358 ATL INTERNALS IConnectionPoint ISomeSink Sink ISomeSink Sink Figure 8.1. A connection to each of two sinks HRESULT Advise ([in] IUnknown* pUnkSinkt [out] DWORD* pdwCookie) HRESULT Unadvise ([in] OWORD dwCookie); HRESULT EnumConnections ([out] IEnumConnections** ppEnum};- The CetConnecti o n ln te r f ace method returns the interface identifier (IID) of the sink interface for which a connection point makes calls. Using the prior example, calling CetConnecti onlnte rf ace would return IID 一 ISomeSink. A client calls the Advi se method to establish a connection. The client provides the 叩 propriate sink interface pointer for the connection point and receives a magic cookie (token) that represents the connection. A client can later call the Unadvi se method, speci­ fying the magic cookie to break the connection. The EnumConnecti ons method re­ turns 汪 standard COM enumeration object that a client uses to enumerate all the current connections held by a connection point The last method is CetConnec­ t i onPoi ntC ontai ner, which introduces a new complexity. So far, this design allows a source object to make calls on only one specific in­ terface. The source object maintains a list of clients that wish to receive calls on that specific interface. When the source object determines that it should call one of the methods of its sink interface, the source iterates through its list of sink objects, call­ ing that method for each sink object What the design (again, as described so far) doesn’t include is the ability for an object to originate calls on multiple different in­ terfaces using this mechanism. Alternatively, to present the question directly, we have a design in which an object can support multiple connections to a single con­ nection point, but how can an object support multiple different connection points? The solution is to demote the source object to subobject status and have an en­ capsulating object, called the connectable object (Figure 8.2), act as a container of these source subobjects. A client uses the source object’s CetConnecti onPoi n t­ C ontai ner method to retrieve a pointer to the connectable object A connectable object implements the IConnecti onPoi ntContai ner interface. CONNECTION POINTS 359 IConnectionPointContainer Connectable Object IConnectionPoint IConnectionPoint Figure 8.2. A connectable object Implementing IConnectionPointContainer indicates that a COM object supports connection points and, more specifically, that it can provide 狂 connection point (a source subobject) for each sink interface the connectable object knows how to call. Clients then use the connection point as described previously to estab­ lish the connection. The IConnectionPointContainer Interface Here is the definition of the IC onnecti onPoi ntC ontai ner interface: interface IConnectionPointContainer : IUnknown HRESULT EnumConnectionPoints ([out] IEnumConnectionPoints ** pp£num); HRESULT FindConnectionPoint ([in] REFIIO You call the FindConnectionPoint method to retrieve an IConnectionPoint interface pointer to a source subobject that originates calls on the interface specified by the r i i d parameter. The EnumConnecti onPoi nts method returns a standard COM enumerator subobject that implements the IEnumConnecti on­ Poi nts interface. You can use this enumerator interface to retrieve an ICon­ nectionPoint interface pointer for each connection point supported by the connectable object [out] IConncctionPoint*# ppCP); 360 ATL INTERNALS Most often, a client that wishes to establish a connection to a connectable ob­ ject does the following (with error checking removed for clarity): CComPtr pSource = /* Set to the source of the events V ; CComPtr<_ISpeakerEvents> pSink = /* Sink to receive the events */ ; DWORD dwCookie ; CComPtr pcpc; HRESULT hr * pSource.Querylnterface C&pcpc); CComPtr pep ; hr = pcpc->Fi ndConnecti onPoi nt (— uuidof(_ISpeakerEvents), & p c p ) ; hr - pcp->Advise (pSink, AdwCookie) ; // Establish connection II Time goes by, callbacks occur... hr = pcp->Unadvise (dwCookie) ; // Break connection In fact, ATL provides two useful functions that make and break the connection between a source and a sink object. The A tlA dvi se function makes the connection between a connectable object’s connection point (specified by the pUnkCP and iid parameters) and a sink interface implementation (specified by the pUnk parameter) and returns a registration code for the connection in the pdw location. The AtlUn- advi se function requests the connectable object’s connection point to break the connection identified by the dw parameter. ATLAPI AtlAdvise(IUnknown* pUnkCP, IUnknown* pUnk, const IID& iid, LPDWORD pdw); ATLAPI At1Unadvise(IUnknown* pUnkCP. const IID& iid, 0W0RD dw); You would use these functions like this: DWORD dwCookie; II Make a connection hr = AtlAdvise (pSource, pSink, —uuidof (一ISpeakerEvents)• AdwCookie); // . . . Receive callbacks . . . // Break the connection hr = AtlUnadvise (pSource, —uuidof (—ISpeakerEvents), dwCookie); In summary, to establish a connection, you need an interface pointer for the con­ nectable object, an interface pointer to an object that implements the sink interface, and the sink interface ID. CONNECTION POINTS You will commonly find corm(、c.tion points used by ActiveX controls. An Ac­ tiveX control is often a sourxv of events. An event source implements event c.all- hacks as method calls on a specified event sink interface. Typically, an ActiveX control container will implement an event sink interface so that it can receive specific events from its contained controls. Using the connection points protocol, the control container establishes a connect ion from the source of events in the con­ trol (the connection point) to the event sink in the container. When an event occurs, the connection point calls (he appropriate method of the sink interface for each sink connected to the connection point. I should point out that the connection points design is deliberately a very gen­ eral mechanism, which means using connection points isn’t terribly efficient in some cases.1 Connection points are most useful in the case in which an unknown number of clients may wish to establish callbacks to a variety of different sink in- lorfacos. In addition,the connection points protocol is well known; therefore, ob­ jects with no custom knowledge of eac h other can use it to establish connections. If you are writ ing both the source and sink objects, you may wish to invent a cus­ tom protocol, which is easier to use than the connection point protocol, to trade in­ terface pointers. Creating an ATL-Based Connectable Object The Brilliant Example Problem That Produces Blinding Insight Let’s create a Demagogue COM object. It represents a public speaker. The ATL- based CDemogogue class will implement the I Speaker interface. When asked to Speak, a speaker can whisper, talk, or yell his or her Speech depending on the value of Vol ume. interface ISpeaker : IOi spatch { [propget, id(l)] HRESULT [propput, i'd(l)] HRESULT [propget, id(2)] HRESULT [propput, id(2)] HRESULT [id(3)j HRESULT Speak(); Vo1ume([out Volume([in] Speech([out Speech([in] retval] long *pVal) long newVal)'/ retval] BSTR ^pVal) BSTR newVal); 1 For ActiveX controls and other in-process objects, connection points are acceptable, hut when round- trips are a concern, they arc horrid. Head Effectivr COM by Don Box, Keith Browii, Tim EwaJd, and Chris Sells (Addison-Wesley, 1998) for an in-, public CComCoClass, public ISupportErrorlnfo, CONNECTION POINTS public IDispatchlmpl, Seven Steps to a Connectable Object There are seven steps to creating a connectable object using ATL. 1. You need to implement the IConnecti onPoi ntContai ner interface. 2. Querylnterface should respond to requests for IID_IConnecti onPoi n t­ C ontai ner. 3. You need to implement the IConnecti onPoi nt interface for each source in­ terface supported by the connectable object. 4. You need to provide a connection map, that is, a table that associates an IID with a connection point implementation. 5. You must update the coclass definition for the connectable object class in your IDL file to specify each source interface. Each source interface must have the [source] attribute. The primary source interface should have the [de­ fa u lt, source] attributes. 6. Typically, you will want helper methods that call the sink methods for all con­ nected sinks. 7. You must call the helper methods at the appropriate times. Adding Required Base Classes to Your Connectable Object In order for an object to fire events using the connection points protocol, the object must be a connectable object. This means the object must implement the ICon­ n e c ti onPoi n tC o n ta i ner interface. You can use an ATL-provided implementa­ tion of IConnecti onPoi ntContai ner and IConnecti onPoi ht by deriving the connectable object class from the appropriate base classes. Step 1. Derive the CDemagogue connectable object class from the ATL template class IConnecti onPoi ntContai nerlmpl. This template class requires one pa­ rameter— the name of your derived class. This derivation provides the connectable object with an implementation of the IConnecti onPoi ntContai ner interface. class ATL_NO_VTABLE CDemagogue : AT L INTERNALS public IConnect i onPoi ntContai nerlmpl, Changes to the C〇 M_MAP for a Connectable Object Step 2. Any time you add a new interface implementation to a class, you should immediately add support for the interface to your Query I nterface method. ATL implements Query In te rfa c e for an object by searching the object’s COM_MAP for an entry matching the requested IID. To indicate that the object supports the ICon­ n e c ti onPoi n tC o n ta i ne r interface, add a COM_INTERFACE_ENTRY macro for the interface: BECIN_COM_MAP(CDemagogue) 參參籲 COM_INTERFACE_ENTRY(IConnectionPointContainer) • • • END_COM_MAP () Adding Each Connection Point A connection point container needs a collection of connection points to contain (otherwise, the container is somewhat boring as well as misleading). For each source interface that the connectable object supports, you need a connection point subobject. A connection point subobject is logically a separate object (that is, its COM object identity is unique) that implements the IConnecti onPoi nt interface. Step 3. To create connection point subobjects, you derive your connectable object class from the template class IConnecti onPoi ntlm pl one or more times— once for each source interface supported by the connectable object. This derivation provides the connectable object with one or more implementations of the ICon­ nectionP oint interface on separately reference-counted subobjects. The ICon- nectionPoi ntlm pl class requires three template parameters: the name of your connectable object class, the IID of the connection point’s source interface, and, op­ tionally, the name of a class that manages the connections. class ATL_NO_VTABLE CDemagogue : * • • public IConnectionPointContainerlmpl, public IConnect i onPoi ntlmpl^Demagogue, &DIID 一 ISpeakerEvents> CONNECTION POINTS Where, Oh Where Are the Connection Points? Where, Oh Where Can They Be? Any implenienlation of IConnecti onPoi ntContai ner needs some fundamental information: a list of connection point objects aiul the IID supported by each con­ nexion point object. 1*he ATL impleinenralion uses a (able called a connection p o in t )nnp in wluch you provide !h(* ren point map in your connectable object’s class declaration using three ATL macros. The BEGIN. CONNECTION_POINT_MAP macro specifies the beginning of the The onl.v parameter is the class name of the connectable object Each CON- Nfc ( f JON_.POINr_.ENTRY macro places an entry in the table and represents one con- point. Hu. macro s only parameter is the IID of the interface supported by rhe connectioii point. Note that the CONNECTION—POINT一ENTRY macro requires you to specify an III), whon*as tlu^ COM_INTERFACE_FNTRY macro needs an interface class name. Historically, you could always prepend an IID 〜 prefix to an interface class name to produce tlie name of the GUID for the interface. Prior versions of ATL's C0M_ INTtRFACE—ENTRY macro actually did this to produce the appropriate 11D. However, sc>urc e interfaces have no such regular naming convention. Various versions of MFC, MKTYPUB, and MIDL have generated different prefixes to a di sp i n te r f ace. The C0NNECTI0N_P0INT_ENTRY macro thus couldn't, assume a pn*fix and therefoie required you to specify the IID explicitly. A.s of veision 3.0, ATL uses, by default, the — uui dof keyword to obtain the IID foi a class. [Jn for innately, changing the C0NNECTI0N_P0INT_ENTRY macro to ex- p i1. i -s class n;uii(> would break existing rode. Th(* t ND. l ONNtCTI0N_P0INT_MAP macro generates an end-of-table marker aiu) sialic nu inber function that returns the address of the connection map and sf. Iieros tlie connection map for the CDemagogue class. BEGIN.CONNECTION ..POINT^MAP(CDemagogue) C:0NNt:C.TI0N POINT„CNTRY(DIIO__ISpeakerEvents) » Nt)^C0NNEC 110N__ POT NT.MAP () Update the Coclass to Support the Source Interface Step 5. Clients often read the type library, which describes an object that is a sotirco of cvonrs, in onier to dotennino certain implementation details, such as the objects source iiitert'ace(s). You’ll need to ensure that the source object's c o c la s s desc ription is up-to-date by adding an entry to describe the source interface. 366 ATL INTERNALS coclass Demagogue { [default] interface interface interface [default, source] dispinterface IUnknown; ISpeaker; INamedObject; .ISpeakerEvents; Where There Are Events, There Must Be Fire So far, we have a Demagogue connectable object that is a container of connection points, and one connection point. The implementation, as presented up to this point, permits a client to register a callback interface with a connection point. All the enumerators will work. The client can even disconnect. However, the con­ nectable object never Issues any callbacks. This isn’t terribly useful and has been a bit of work for no significant gain, so we’d better continue and finish things off. A connectable object needs to call the sink interface methods, otherwise known as firing the events. To fire an event, you call the appropriate event method of the sink interface for each sink interface pointer registered with a connection point. This task is complex enough that you’ll generally find it useful to add event firing helper methods to your connectable object class. You’ll have one helper method in your connectable object class for each method in each of your connection points,supported interfaces. You fire an event by calling the associated event method of a particular sink in­ terface. You do this for each sink interface registered with the connection point. This means you need to iterate through a connection point’s list of sink interfaces and call the event method for each interface pointer. “How and where does a con­ nection point maintain this list?” you ask. Good timing— I was about to get to that. Each IConnecti onPoi ntlm pl base class object (which means each connec­ tion point) contains a member variable m_vec that ATL declares as a vector of IUn­ known pointers. It actually is a vector of sink interface pointers of the appropriate type. For example, the vector in the connection point associated with DIID_ ISpeakerEvents actually contains 一 SpeakerEvents pointers. You don’t need to call Querylnterface for DIID—ISpeakerEvents after retrieving each sink pointer. ATL’s implementation of IConnecti onPoi n tlm p l:: Advi se has already performed this query for you. By default, m一 vec is a CComDynami cUnkArray object, which is a dynamically allocated array of IUnknown pointers, each a client sink interface pointer for the connection point. The CComDynami cUnkArray class grows the vector as required, so the default implementation provides an unlimited number of connections. CONNECTION POINTS Alternatively, when you declare the IConnecti onPoi ntlm pl base class you (,aii specify that m_vec is a CComUnkArray object that holds a fixed number of sink interface pointers. I Ise the CComUnkArray class when you want to support a fixed maximum number of connections. ATL also provides an explicit template, CCom- UnkArray, that is specialized for a single connection. Step 6. To fire an event, you iterate through the array and, fo r each won-NULL en­ try, call the sink interface method associated with the event you wish to fire. Here’s a simple helper method that fires the OnTalk event of the 一 SpeakerEvents interface. Note that m_.vec is only unambiguous when you have a single connection point. HRESULT Fire..OnTalk(BSTR bstrSpeech) { CComVariant arg, varResult; int nlndex, nConnections = m_vec.GetSize(); for (nlndex = 0; nlndex < nConnections; nlndexf+) { CComPtr sp * m_vec.GetAt(nlndex); IDispatch* pDispatch = reinterpret 一 cast(sp.p); if (pDispatch != NULL) { VariantClear(«&varResul t); arg = bstrSpeech; DISPPARAMS disp = { &arg, NULL, 1, 0 }; pDi spatch->Invoke(0x2, IID—NULL, LOCALE_USER_DEFAULT, DISPATCH—METHOD, &disp, «&varResult, NULL, NULL); } } return varResult.scode; } The ATL Connection Point Proxy Generator Writing the helper methods that call back a connect ion point interface method is tedious and prone to errors. An additional complexity is that a sink interface can be a custom COM interface or a d i sp i n te rfa c e . There is considerably more work to making a di spi nterface callback (surh as, using ID ispatch:: Invoke) than making a vtable callback. Unfortunately, the di spi nterface callback is the most frequent case because it’s the only event, mechanism supported by scripting lan­ guages, Internet Explorer, and most ActiveX control containers. ATL INTERNALS The Visual C++ IDE, however, provides a source code generation tool that generates a connection point class that contains all the necessary helper methods for making callbacks on a specific connection point interface. In the Visual C + + ClassView pane, right-click on the C+ + class that you want to be a source of events. Select the Implement Connection Point menu item from the context menu. The Im­ plement Connection Point dialog appears (Figure 8.3). The Implement Connection Point dialog creates one or more classes (declared and defined in the specified header file) that represent the specified interface(s) and their methods. To use the code generator, you must have a type library that describes the desired event interface. The code generator reads the type library description of an interface and generates a class, derived from IConnecti on­ Poi n t lm p l,that contains an event firing helper function for each interface method. You specify the generated class name as one of your connectable object’s base classes. This base class implements a specific connection point and contains all necessary event firing helper methods. The Implement Connection Point Proxy Generated Code The proxy generator produces a template class with a name in the form CProxy. This proxy class requires one parameter: your connectable object's class name. The proxy class derives from an IC onnecti on- Implement Connection Point ATUNTERNALSUb | Ete rvsme: jSpeakerE ventsCP.h Qrovvse. interfaces □ _l ClimbableO bjectE vents □ JSpeakerE vents t i n OK | AddTypeib., Figure 8.3. The Implement Connection Point dialog CONNECTION POINTS 369 Poi ntlm pl template instantiation that specifies your source interface ID and the dynamic array connection manager. Here is the code generated for the previously described _ISpeakerEvents in­ terface by the Implement Connection Point proxy generator #ifndef .SPEAKEREVENTSCP^H_ #define >SPEAKEREVENTSCP_M_ template class CProxy_ISpeakerEvents : public IConnectionPointlmpl { //Warning this class may be recreated by the wizard. public: HRESULT Fir€_OnWhisp€r(BSTR bstrSpeech) 4 : CComVariant varResult| ; T* pT • static.cast(thi$ ) ; : S int nConnectionlndex; CComVariant* pvars - new CComVariant[l}; 1nt nConnections _ nuvec.CetSlzeC); for (nConnectionlndex * 0; nConnectionIndex < nConnections nConnectionIndex++) { pT->Lock(); CCcMi»Ptr sp - nuvec .CetAt(nConnectionlndex); pT->Unlock(); IDispatch* pOispatch * (pDi spatch NULL) { VariantClearC&varResult); pvars[0] ■ bstrSpeech; DISPPARAMS disp « { pvars, NULL, pOi spatch->Invoke(Oxl, IID^NULL, LOCALE«USEIL.DEFAULT OISPATCK-METHOO. Adisp, AvarResult. NULL, MULL); } > delete[] pvars; return varResult.scode; } // Other methods similar, deleted for clarity rei nterpret_cast<10ispatch*>(sp•p) • 參 tjo.气•, ♦• X tendif 370 ATL INTERNALS Using the Connection Point Proxy Code Putting everything together so far, here are the pertinent parts of the CDemagogue connectable object declaration. The only change from previous examples is the use of the generated connection point proxy class, CProxy 一 ISpeakerEventscCDem- agogue>, as a base class for the connection point instead of the more generic IConnecti onPoi ntlm pl class. class ATL_N0_VTABLE CDemagogue : public IConnectionPointContainerlmpl, public CProxy 一 ISpeakerEvents, BECIN_COM_MAP(CDemagogue) • • • COM_INTERFACE_ENTRY(IConnectionPointContainer) END_C0M_MAP() BECIN_CONNECTION_POINT_MAPCCDemagogue) CONNECTION_POINT_ENTRY(DIID— ISpeakerEvents) END_C0NNECTI0N_P0INT_MAP() Firing the Events Step 7. The final step to make everything work is to fire each event at the appro­ priate time. When to do this is very application specific, but here is one example. The CDemagogue object makes its speech when a client calls the Speak method. The Speak method, based on the current volume property, cither whis­ pers, talks, or yells. It does this by calling the OnWh i sp e r, OnT a l k, or On Y e l l event method, as appropriate, of all clients listening to the demagogue's —Speaker- Events interface. STDMETHODIMP CDemagogue::Speak() { if (m一 volume <= -100) return Fire_0nWhisper («_speech); CONNECTION POINTS if (m_volume >= 100) return Fire_OnYell Ca.speech) return Fi re_OnTalk (M_speech); on >b- nt • tloo le ec bled : n ient c 论 n M n n v e t ho m 过 irc c o K:t oni . de bje c pl eob 戮 1 d p th e mr by l that shol Bell | l ect l tB ple 3 bj up a s叩 c ho s u ^ m c e s-:= l vide T e mte 0 r o. elss . elr A d叩 e — Dfals lse. Milri lll tst o n r t * 1\«1 6 n o m - a t M rmat] 2 d d r orl ^ l o toc erf o o in f the to 叩 c h n te 咖 • l re L rt : € G o Th^ Ject acc More specifically, clients that wish to receive events from a connectable object may ask the object for its IP rovi deCl assInfo2 interface. Microsoft Inter­ net Explorer, Visual Basic, and ATL-based ActiveX control containers do this, for example. The client calls the GetGUID method of this interface with the parameter GUIDKIND_DEFAUL7LS0URCE_DISP_I^D to retrieve the HD of the primary event dispinterface supported by the connectable object H\is is the ED of the d ispinterface listed in the connectable object’s coclass description with the [default, source] attributes. Supporting IP rovi deCl assInfo2 allows arbitrary clients a convenient mech­ anism for determining the primary event source IID and then using the IID to es­ tablish a connection. Note that the IID returned by this call to GetGUID must be a di spi nterface. It cannot be a standard IUnknown-derived (vtable) interface. When a connectable object fails the query for IProvi deCl asslnfc2, some clients will ask for IP rovi deCl assln fo . A client can use this interface to retrieve an IT ypelnfo pointer about the connectable object's class. With 艺 considerable bit of effort, a client can use this IT ypelnfo pointer and determine the default source interface, supported by the connectable object The IP rovi deCl assInfo2 interface derives from the IP rovi deCl assln fo interface, so by implementing the former interface, you’ve implemented the latter one. Because most conn objects should implement the IProv^deClass- Info2 interface, ATL provides a template class for the implementation, IP rovi de- C1assInfo2Imp1. IProvideClassInfo2Im pl provides a default implementa­ tion of all the IProvi deCl asslnfo and IProvi deCl asslnfo2 methods. The declaration of the class looks like this: |template< const CLSID* pcoclsid, const IID* psrcid, const GUID* plibid. WORD wMajor - 1, WORD wMinor = 0 , class tihclass * CComTypeInfoHo1der >'' 1 cl ass IProvi deClassInfo2I»npl : public IProvi deC1assInfo2 372 ATL INTERNALS To use this implementation in your connectable object, you must derive the con­ nectable object class from the IProvideCIassInfo2Im pl class. The last two template parameters in the following example are the m3jor and minor version numbers of the component's type library. They default to 1 and 0,respectively, so I didn’t need to list them. However, when you change the type library’s version num­ ber, you also need to change the numbers in the template invocation. You won’t get a compile error if you fail to make the change, but things won’t work correctly! By always listing the version number explicitly, I find I remember to make this change more often. #define LIBRARY_MA30R 1 #define LIBRARV_MIN0R 0 class ATL_NO_VTABLE CDemagogue : • • • public IConnectionPointContainerlmpl, public CProxy_ISpeakerEvents, public IProvideClasslnfo2Impl<&CLSID_Demagogue, &OIID—ISpeakerEvents• &UBIDJMLINTERNALSLib, LIBRARY_MA30R, LIBRARYJ1INOR> ... You also need to update the C0M_MAP so Q uerylnterface responds to IP rovi de­ Cl asslnfo and IProvideClassInfo2. BEGIN_COM_MAP(CDemagogue) • • • COfLINTERFACE_ENTRY(IProvi deClasslnfo2) COM_INTERFACE.ENTRY(IProvideClasslnfo) END—C0M_MAP() Finally, here are all connectable-object-related changes in one place: #define LIBRARY_MA〕OR 1 #define LIBRARY_MINOR 0 II Event dispinterface dispinterface _ISpeakerEvents properties: methods: CONNECTION POINTS 373 [id(l)] HRESULT OnWhisper(BSTR bstrSpeech); [id(2)] HRESULT OnTalk(BSTR bstrSpeech); [id(3)] HRESULT OnYell(BSTR bstrSpeech); }; II Connectable object class coclass Demagogue { [default] interface interface interface [default, source] dispinterface > ; // Implementation class for coclass Demagogue class ATL_N0_VTABLE CDemagogue : public IConnectionPointContainerlmpl, public CProxy_ISpeakerEvents, public IProvideCIassInfo2Xmpl<&CLSID—Demagogue, &OIID— ISpeakerEvents, &LIBID_ATLINTERNALSLib, LIBRARY_MAJOR, LIBRARY_MINOR> ... { public: BEGIN_COM_MAP(CDemagogue) COM„INTERFACE_ENTRY(IConnectionPointContainer) C0M_INTERFACE_ENTRY(IProvideClassInfo2) COM,INTERFACE_ENTRV(IProvideC1assInfo) • • • END_COM_MAP() BECIN_CONNEaiON_POINT_MAP(CDemagogue) CONNECTION_POINT_ENTRY(DIID_ISpeakerEvents) END_CONNECTION_POINT^MAP() Creating an Object That fs an Event Recipient IUnknown; ISpeaker; INamedObject; .ISpeakerEvents; It is quite easy, in theory, to implement an object that receives events on a single in­ terface. You define a class that implements the interface and connect the object to the event source. We have a Demagogue class that generates events on the ATL INTERNALS _I5peakerEvents dispatch interface. Let’s define an EarPoli tic class (clearly one ear of the body politic) that implements _ISpeakerEvents. coclass EarPolitic { [default] dispinterface _XSpeakerEvents; }: Now implement the class using ATL as the CEarPol i t ic class, class ATL_NO_VTABLE CEarPolitic : public _ISpeakerEvents, BECIN_COM_MAP(CEa rPolitic) COM_INTERFACE_ENTRY(IDispatch) COM 一 INTERFACE—ENTRY(—ISpeakerEvents) END_COM_MAP() // _ISpeakerEvents STDMETHOD(GetTypelnfoCount)(UINT* pctinfo); STDMETHODCGetTypelnfo)(UINT itinfo, LCID lcid, ITypelnfo^* pptinfo); STDMETHOD(CetlDsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, OISPID* rgdispid); STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr); Unfortunately, an event interface is typically a di spi nterface, so this requires that we implement the ID i spatch interface and support, at a minimum, the Invoke method. Invoke is a tedious method to write for any n« 1 * • 1 event interface. Another alternative is to use the IDispatch int mplementation pro­ vided by the IDi spatchlm pl template class. Unfortunately, the template class requires parameters describing a dual interface, not a di spi nterface. To use public CONNECTION POINTS 375 ID i spatchlm pl, you need to define a durniny dual interface that has the same dispatch methods, dispatch identifiers, ami function signatures as the event dispinterface. This has more implications than are usually apparent. A d i sp i n te r fa c e is not immutable, unlike a regular COM interface. If you don’t control the definition of liie d i sp i n te rfa c e , it might change from release to release. (Yes, it’s not that likely to change, but it is possible.) This means your dual intorfaoe needs to change as well. This implies that you cannot document the dual interface, because once it is published it is immutable because some client may implement it. Because you shouldn't describe the dual interface in a type library (because that documents it), you cannot use the universal type-library-(Jrivcn marshaler and need a romoting proxy/stub for the dual interface. These are all theoretical issues because the dual interface in this case is an implementation detail specific to the implementation class, but they give motivation enough to look for another solution. On a slightly different note, what if you want to receive the same events from more than one event source and you want to know which source fired the event? For example, lets say you want to implement an Ear Pol i t i c class that acts as a judge listening to 一 Speaker Events from both a Defendant object and a Plaintiff object. Each object is a source of OnWhi sper, OnTal k, and OnYel 1 events, but the judge needs lo keep track of whu is saying what. This requires you to implement the 一 SpeakerEvents interface multiple times — once for each event source. Providing separate implementations of any interface nmltiple times in a class requires cach implementation to be in a different context (that is, in a separate COM identity). Two lypical solutions to this problem are member-wise composition (where each implementation is in a nested class) an(J something similar to tear-off interfaces (where each implementation is in a separate class). The IDispEventlmpI and IDispEventSimplelmpI Classes ATL provides two template classes, IDispEventlmpI and IDispEventSimple- Im pl,that provide an implementation of the ID i spatch interface for an ATL COM object. Typically,you will use one of these classes in an objcct that wishes to receive event callbacks. Both classes implement the dispatch interface on a nested class ob- jert that maintains a separate COM identity from the deriving class. This means you can derive from these classes multiple times when you need to implement multiple sourc e dispatch interfaces. The ID i spEventlmpl class requires a type library that describes the dispatch interface. The class uses the ty p ei nfo at runtime to map the VARIANT parameters received in the Invoke method call to the appropriate types and stack frame layout necessary for calling the event handler member function. 376 ATL INTERNALS template class ATL^NO^VTABLE IOispEventlmpl : public IDispEvcntSimplelmpl The ID i spEventSimpl elmpl class doesn’t use a type library, so it’s a lighter- weight class. You use it when you don’t have a type library or when you want to be more efficient at runtime. template class ATL.NO.VTABLE IOispEventSimplelmpl : public _I0ispEventLocator When using the ID i spEventSi mpl elmpl class, you must provide an _ATL_ FUNCLINFO structure containing information that describes the expected parame­ ters for the event handler. struct JVTL^FUNCINFO { CALLCONV cc; VARTYPE vtReturn; SHORT nParams; // Calling convention // VARIANT type for return value // Number of parameters // Array of parameter VARIANT type VARTYPE pVarTypes [JML.MAXLVARTYPES] statically id later in Notice that ID i spEventlmpl derives from the ID i spEventSi mpl elmpl class. It’s the ID i spEventSi mpl elm pl class that calls the event handler based on the infor­ mation in an _ATL-FUNC_INF0 structure. You can provide the structure (at compile time) by referencing the structure in the sink map (described this chapter). When you provide no structure reference, the ID i spEventSi mpl elmpl class calls the virtual method CetFuncInfoFrom ld to get an _ATL_FUNC_INFO struc­ ture for the event handler associated with the 印 ecified DISPID. You provide the structure dynamically by overriding CetFuncInfoFromld and returning the ap­ propriate structure when called. You must use CetFuncInfoFromld when you want to call different event methods based on the locale provided by the event source. CONNECTION POINTS 377 Here’s the default implementation provided by the IDi spEventSimpl elmpl class: //Helper for finding the function index for a DISPID virtual HRESULT CetFuncInfoFromId(const IID& iid , DISPID dispidMember, LCID Icid. *_ATL—RJNCLIMFO& info) { return E.NOTIMPL; The ID i spEventlmpI class overrides this virtual method to create the structure from the typei nfo in the specified type library. Implementing an Event Sink The easiest way to implement one or more event sinks in an ATL object is to derive the object one or more times from ID i spEventlm pI一 once for each unique event interface coming from each unique event source. Here’s the template class specifi­ cation once more: template class A T U N C L V T A B U IDi spEventlmpI^ ; public IDi spEventSi mplelmpl The nID parameter specifies an identifier for the event source that is unique to the deriving class T. You’ll see in Chapter 10, ActiveX Controls, that when the event source is a contained control and the event recipient is a composite control, the identifier is the contained control’s child window identifier. A composite control can default all other template parameters, but an arbitrary COM object must specify all parameters except the last The pdi i d parameter specifies the GUID for the event d i spi nterface that this class implements. The dispinterface must be described in the type library specified by the p iib id parameter and the type library m^jor and minor version numbers, wMajor and wMi nor. The t i hcl ass parameter specifies the class to manage the type informa­ tion for the deriving class T. The default CComTypelnfoHol der class is generally acceptable. The more efficient way to implement one or more event sinks in an ATL object uses the ID i spEventSi mpl elm pl class and needs no type library at runtime. You 378 ATL INTERNALS must, however, provide the necessary _ATL_FUNC_INFO structure, as described previously. When using the ID i spEventSi mpl elmpl class, you need specify only the nID event source identifier, the deriving class T, and the pdi i d GUID for the event di spi nte r f ace: template class ATL_NO.VTABLE IOispEventSimplelmpl : public .IDispEventlocator Let’s redefine the CEarPol i t i c class to implement the _SpeakerEvents dis­ patch interface twice: once for a Demagogue acting as a Defendant and once for a different Demagogue acting as a Plaintiff. I have a type library, so I’ll use the ID i spEventlm pl class to implement the sink for the Defendant obj ect’s 一 Speaker- Events callbacks. I’ll use the IDi spEventSi mpl elmpl class for the Plaintiff object’s 一 SpeakerEvents callbacks so I can demonstrate the alternative imple­ mentation. I typically introduce a typedef for each event interface implementation to minimize typing and mistakes. static const int DEFENDANT_50URCE_ID = 0 ; static const int PLAINTIFF_SOURCE_ID = 1 ; class CEarPolitic; typedef IDi spEventlmpl DefendantEventlmpl; typedef IDi spEventSi mplelmpl PIai ntiffEventlmpl; In this example, I arbitrarily chose 0 and 1 for the event source identifiers. The identifiers could have been any numbers. Now we need to derive the CEarPol i t i c class from the two event implemen­ tation classes: class ATL—NO_VTABLE CEarPolitic : • • • public DcfendantEventlapl, public PIai nti ffEventlnpl {- // Event sink map required "in here CONNECTION POINTS 379 The Event Sink Map The IDi spEventSi mpl elmpl class’s implementation of the Invoke method re­ ceives the event callbacks. The event source, when it calls the Invoke method, specifies the event that has occurred, using the DISPID parameter. The ID i sp­ EventSi mpl elmpl implementation searches an event sink table for the function to call when event DISPID occurs on dispatch interface DIID from event source identifier SOURCE. You specify the beginning of the event sink map using the BEGIN一 SINK__MAP macro within the declaration of the class that derives from ID i spEventlmpI and/ or ID i spEventSi mpl elm pl. You map each unique SOURCE/DIID/DISPID triple to the proper event handling method using the 5INK_ENTRY, the SINK_ENTRY_EX, and the SINK_ENTRY_INFO macros. _ SINK_ENTRY(SOURCE, DISPID, func): Use this macro in a composite con­ trol to name the handler for the specified event in the specified contained con­ trol's default source interface. This macro assumes the use of ID i sp E v e n t­ lmpI and assumes that you call the A tl Advi seSi nkMap function to establish the connection. The A tl Advi seSi nkMap function assumes that your class is derived from CWi ndow. All in all, the SINK_ENTRY macro isn’t very useful for non-user-interface (UI) objects wishing to receive events. ■ SINK_ENTRY_EX(SOURCE f D IID , D ISPID , fu n c ): Use this macro to indicate the handler for the specified event in the specified object’s specified source in­ terface. This macro is most useful for non-UI objects wishing to receive events and for composite controls wishing to receive events from a contained control’s nondefault source interface. ■ SINK_ENTRY_INFO(SOURCE, DIID, DISPID, func, info): This macro is similar to the SINK_ENTRY_EX macro with the addition that you can specify the _ATL_FUNC_INFO structure to be used when calling the event handler. You typically use this macro when using the ID i spE ventSi mpl elm pl class. You end the event sink map using the END_SINK_MAP macro. The general form of the table is as follows: BEGIN_SINICMAP(CEarPol i ti c) SINK_ENTRY^EX(SOURCE, DIID, DISPID, EventHandlerFunc) SINK_ENTRY_INFO(SOURCE, DIID, DISPID, EventHandlerFunc, Ainfo) • • • END 一 SINIC_MAP() In the C E arP olitic example, there are three events, all from the same dis­ patch interface but coming from two different event sources. Therefore, we need six 380 ATL INTERNALS event sink map entries. I can use the SINK__ENTRY_£X macro to identify the event handlers for the Defendant event source. I don’t need to specify the _ATL_FUNC_ INFO structure because the ID i spEventlm pl base class will use the type library to provide the appropriate structure at runtime. I need to use the SINK_ENTRY_ INFO macro for the Plaintiff object. Because I used the ID i spEventSi mpl elmpl base class, I need to provide the function information structure describing each event method. Each function information structure describes one event method. The structure contains the calling convention, the VARIANT type of the return value of the method, the number of parameters, and the VARIANT types of each of the parame­ ters. The calling convention should be set to CC^STDCALL because that’s what the ID i spE ventSi mpl elm pl class expects the event handlers to use. struct J\TU.FUN(LINF0 { CALLCONV cc; VARTYPE vtReturn; SHORT nParams; VARTYPE pVarTypes[JVTL.MA)CVARTYPES]; // Calling convention // VARIANT type for return value // Number of parameter^ // Array of parameter VARIANT type Here are the function prototypes for the Plaintiff’s three event methods and their information structures (all identical in this example). Event handlers typically do not return a value; that is, they are v o i d functions. Note: The proper vtReturn value in the _A T I F U N C _ I N F 0 structure to represent a voi d function return is VT_EMPTY, not VT_V0ID. void 一 stdcall 0nHearP1aintiffVhisper(BSTR bstrText); void —stdcall OnHearPlaintiffTalk(BSTR bstrText); void — stdcall OnHearPlai nti ffYel1 (BSTR bstrText); static const int DISPID.WHISPER static const int DISPID.TALK static const int DISPID—YELL -ATL.FUNC.INF0 OnHearWhisperlnfo ■ {CCLSTDCALL, VT.EMPTY, 1, _ATL_FUNC_INFO OnHearTalklnfo ■ {CC-STDCALL, VT.EMPTY, 1, VT 一 BSTR }} VT 一 BSTR }} CONNECTION POINTS 381 _ATl..FUNCLINFO OnHearVellInfo pno = punk; CComBSTR name; HRESULT hr = pno->get_Name (&name); text.AppendBSTR (name); text + 篇 OLESTR (••)"); text += strAction; } CONNECT 丨 ON POINTS 383 Connecting the Event Sink to an Event Source When yovu* class is a l ompositt' n »htrol, you should use the A tl Advi seSi nkMap function (o establish and remove tlie inline HRESULT AtlAdvi seSi nkMap(T* pT, bool bAdvise); You rn ust use (he A tl Advi seSi nkMap function to establish the connections aiiy- tinir you use tho ID i spEventlm pl class and you specify only the first two template parameters, using default values for the rest. Not specifying the source interface implies using the default source interface for the event source. It is the A tl Advi se­ Si nkMap method tlia( determines the default source interface, if unspec ified, for eac h event source and establishes the connection point to that interface. When your class isn't a composite control, as in the ongoing example, you must explicitly call the Di spEventAdvi se method of each of your ID i spEvent­ Si mplelmp 1 (or derive(i) base classes to connec t each event source lo each event sink implementation. The pUnk parameter to the Di spEventAdvi se method is any interface on the event source, and \\w pi id parameter is the desired source dis­ patch interface G n i). The Di spEventUnadvn se method breaks the connection. HRESULT Di SpEventAdvi se(IUnknown* pUnk, const IID* pi id); HRESULT OispEventUnadvise(IUnknown* pUnk, const IID* pi id); Here is the IListener interface. I added it to the E arP olitic coclass to provide a means to (letermine if a( OM object can listen to a Defendant and a Plain­ tiff. It also provides rhe Li stenTo and StopLi stem* ng methods to establish and break the connection point between a Speaker event source and the Defendant or Plaintiff event sink. interface IListener : IDispatch typedef enum SpeakerRole { Defendant, P la in tiff } SpeakerRole ATL INTERNALS [id(l)] HRESULT ListenTo(SpeakerRole role, IUnknown* pSpeaker); [id(2)] HRESULT StopListeningCSpeakerRole role); The implementation of these methods is straightforward. ListenTo calls the Di spEventAdvi se method on the appropriate event sink to establish the con­ nection. STDMETHODIMP CEarPolitic::ListenToCSpeakerRole role, IUnknown *pSpeaker) { HRESULT hr = StopLi stening (role) ; // Validates role if (FAILED (hr)) return hr ; switch (role) { case Defendant: hr « DefendantEventlmpl::DispEventAdvise (pSpeaker, &DIID_ ISpeakerEvents); if (SUCCEEDED (hr)) m_defendant = pSpeaker ; e I se Error (OLESTR("The defendant does not support listening"), _ uuidof(IListener), hr); break; case Plaintiff: hr = PIai ntiffEventlrnpl::DispEventAdvise (pSpeaker, &OIID_ ISpeakerEvents); if (SUCCEEDED (hr)) m一 plaintiff = pSpeaker ; el se Error (OLESTR(nThe Plaintiff does not support listening"), _ uuidof(IListener), hr); break; } return hr; The StopLi steni ng method calls Di spEventUnadvi se to break the connection STDMFTHODIMP CEarPoli t i c : :Stopl i stening(SpeakerRole role) HRESULT hr = S^OK ; switch (role) { case Defendant: if (m.defendant) hr = DefendantEventlmpl::DisptvenlUnadvise (m„defendant, &DIID—TSpeakertvents); if FAILED(hr) Error (OLESTR("Unexpected error trying to stop listening to • "the defendant'*), ...uuidof (II istener) , hr); m_defendant - NULL; break; case Plaintiff: if (m. plainti ff) hr - Plainti ffEventlmpl::DispEventUnadvi se (m^plainti ff, &DIID一 ISpeakerEvents); 、 if FAlLED(hr) Error (OLESTR("Unexpected error trying to stop listening to •_ ••the Plaintiff), __uuidof(IListener), hr); m_plaintiff = NULL; break; default: hr : E—INVALIDARG; break; } return hr: In summary,use tlie IDi spEventlmpl and IDi spEventSimplelmpl classes to implomer\t an event sink for a dispatch interface. Call the Di spEventAdvi se and Di spEventlinadvi se methods of each class to establish and break the connection. 386 ATL INTERNALS Derive your class directly from the source interface when the source is a simple COM interface. Call the A tl Advi se and A tl Unadvi se global functions to establish and break the connection. When you need to implement the same source interface multiple times, you’ll need to use one of the various standard techniques (nested composition, method coloring, separate classes, or intermediate base classes) to avoid name collisions. How It All Works: The Messy Implementation Details Classes Used by an Event Source The IConnectionPointContainerlmpI Class Let’s start by examining the IConnecti onPoi ntContai nerlmpl template class implementation of the IConnecti on :Contai ner interface. First, the class needs to provide le compatible with the IConnection- Poi n tC o n ta i ner interface. This vtable must contain five methods: the three IU n­ known methods and the two IConnecti onPoi ntContai ner methods. ///////////////////////////////////////////////////////////////////// CComEnumConnecti onPoints; public: STOMETHOO(EnumConnecti onPoi nts)(IEnumConnectionPoi nts#* ppEnum) if (ppEnum ** NULL) return E^POINTER; #ppEnum * NULL; CCofnEnumConnectionPoints* pEnum * NULL; ATLTRY(pEnum * new CComObj ect) if (pEnum « NULL) return E^OUTOFMEMORY; int nCPCount; const «ATI_•CONNMAP_ENTRY* pEntry T : :CetConnHap(AnCPCount); // IConnectionPointContainerlmpl template class ATL_NO_VTABLE IConnectionPointContainerlmpl public IConnecti onPoi ntContai ner typedef CComEnumdwOffset Is fOWORO')-!') / ppCP[i++] = (IConnectionPoint*)((int)th1s+pEntry-> dwOffset); pEntry++; } II copy the pointers: they will AddRef this object HRESULT hRes » pEnum->Init((IConnectionPoint**)AppCP[0]f (IConnectionPointkfr)&ppCP[nCPCount], reinterpret_cast(this) , AtlFlagCopy); if (FAILED(hRcs)) { delete pEnum; return hRes; } hRes ■ pEnu«->QueryInterface(II0.IEnumConn€ctionPo4ints, (void**)ppEnum); * if (FAILeO(hRes)) delete pEnum; return hRes; } STDMETHOO(Fi ndConnectionPoi nt)(REFIID riid, IConnecti onPoi nt** ppCP) { if CppCP «= NULL) return E—POINTER; ♦ppCP • NULL; HRESULT hRes - CONNECT^E^NOCONNECTION; const ^ATL.CONNMAP.ENTRY* pEntry » T::CetConnMap(NULL); IID iid; while (pEntry->dwOffset !« (DWORD)-1) { IConnectionPoi nt* pCP « (IConnecti onPoi nt#) (Ci nt) thi s+pEnt ry->d«(Of f set); if (SUCC£EDEO(pCP->CetConnectionlnterfaceC&iid))站 InlinelsEqualCUIDCriid. iid)) { *ppCP - pCP; pCP->AddRef(); hRes * S_OK; ATL INTERNALS break > pEntry++; } return hRes; The IUnknown methods are easy. The class doesn't implement them. You bring in the proper implementation of these three methods when you define a CComOb- je c t class parameterized on your connectable object class, for example, CCom- Object. The CComEnumConnecti onPoi nts typedef declares a class for a standard COM enumerator that implements tho IEnumConnecti onPoi nts interface. You use this class of enumerator to enumerate IC onnecti onPoi n t interface pointers. A template expansion of the ATL CComEnum class provides the implementation. The implementation of the EnumConnecti onPoi nts method creates and returns aii instance of this enumerator. EnumConnecti onPoi nts begins with some basic error checking, then creates a new instance of a CComEnumConnecti onPoi nts enumerator on the heap. The ATL enumerator implementation requires that, after instantiation, an enumerator must be initialized. ATL enumerators are rather inflexible in that, to initialize an enumerator, you must pass it an array of the items the enumerator enumerates. In this particular case, the enumerator provides IConnecti onPoi nt pointers, so the initialization array must be an array of IConnecti onPoi nt pointers. A connectable object’s connection map contains the information needed to produce the array of IConnectionPoint pointers. Each connection map entry contains the offset in the connectable object from the base of the IConnecti on­ Poi ntContai nerlmpl instance (that is, the current th is pointer value) to the base of an IConnecti onPoi ntlm pl instance. EnumConnecti onPoi nts allocates space for the initialization array on the stack, using a ll oca. It iterates through each entry of the connection map, calcu­ lates the IConnecti onPoi nt interface pointer to each IConnecti onPoi ntlm pl object, and stores the pointer in the array. Note that these pointers are not reference counted because the lifetime of the pointers in a stack-based array is limited to the method lifetime. The call to the enumerator I n it method initializes the instance. It’s critical here to use the A tl FI agCopy argument. This informs the enumerator to make a proper copy of the items in the initialization array. For interface pointers, this means to Add Ref the pointers when making the copy. The pEnum pointer is a CComEnumConnecti onPoi nts pointer, though it would be a bit better if it were declared as a CComObject pointer because that’s what it actually is. Regardless, EnumConnecti on­ Poi nts must return an IEnumConnecti onPoi nts pointer, not pEnum itself, so it queries the enumerator (via pEnum) for the appropriate interface pointer and re­ turns the pointer. The Fi ndConnecti onPoi nt method is quite straightforward. After the usual initial error checking, Fi ndConnecti onPoi nt uses the connection map to calcu­ late an IConnecti onPoi nt interface pointer to each connection point in the con­ nectable object. Using the interface pointer, it asks each connection point for the IID of its supported interface and compares it with the IID it’s trying to find. A match causes it to return the appropriate AddRef’ed interface pointer with status S_OK; otherwise, failure returns the CONNECT_E_NOCONNECTION status code. Most of the real work is left to the connection point implementation, so let’s look at it next. The IConnectionPointlmpI Class The IConnectionPointlm pI template class implements the IConnecti onPoi nt interface. To do that, the class needs to provide a vtable compatible with the ICon­ necti onPoi n t interface. This vtable must contain eight methods: the three IUn­ known methods and the five IC o n n e c ti onPoi n t methods. The first item of note is that the IConnectionPointlm pI class derives from the _IC P L o cator class. template The _ICPLocator Class. More important, the _ICPLocato r class contains the dec­ laration of the virtual method, ^LocCPQue ry ln te r f ace. A virtual method occupies a slot in the vtable, so this declaration states that calls through the first entry in the vtable, the entry used by callers to invoke Q uerylnterface, will be sent to the method _LocCPQuery In te rfa c e . The declaration is pure virtual, so a derived class needs to provide the implementation. This is important because each connection point needs to provide a unique implementation crf_LocCPQue ry ln te rf ace. template class ATL_NO_VTABLE «ICPLocator { public: //this method needs a different name than Querylnterface STOMETHOO(«LocCPQueryIntcrface)(R£FIIO riid, void ** ppvObject) » 0; virtual ULONC STOMETHOOCALLTYPE AddRef(void) « 0; virtual ULONC STOMETHOOCALLTYPE Release(void) « 0; ATL JNTERNALS A connection point must maintain a COM object identity separate from its connection point container. A connection point therefore needs its own imple­ mentation of Querylnterface. If you named the first virtual method in the «ICP- Locator class “Querylnterface,” C++ multiple inheritance rules would see the name as just another reference to a single implementation of Querylnterface for the connectable object. Normally, that's exactly what you want. For example, you have a class derived from three interfaces. All three interfaces mention the virtual method Q uerylnterface, but you want a single iinplemcntation of the method that all base classes share. Similarly, you want a shared implementation of AddRef and Rel ease as well. But you don't want this for a connection point in a base class. The idea here is that we want to expose two different COM identities (the connectable object and the connection point), which requires two separate imple­ mentations of Querylnterface. but we merge the remaining IUnknown imple­ mentation (AddRef and Release) because we don’t want to keep a separate reference count for each connection point. ATL uses this “unique” approach to avoid having to delegate AddRef and Release calls from the connection point object to the connectable object. The IConnectionPointlmpI Class’s Methods The IUnknown methods are more complicated in IConnecti onPoi ntlm pl than was the case in IConnecti onPoi ntContai nerlmpl so that a connection point can implement its own unique Querylnterface method. For a connection point, this is the _LocCPQueryInterface virtual method. template〈class T, const IID* pi id, class CDV » CComDynamicUnkArray > class ATL_NO_VTABLE IConnectionPointlmpI : public .ICPLocator { typedef CComEnum > CComEnumConnections; typedef CDV >CDV; public: -IConnectionPointlmpI(); STDMETHOOCLocCPQueryInterface)(REFIIO riid, void ** ppvObject) { • if (In1ineIsEqualCUI0(riid, IID^IConnectionPoint) |i IniineIsEqua1Unknown(riid)) { if (ppvObject «» NULL) return E.POINTER; *ppvObject ■ this; AddRef(); »ATLJ)EBUC_INTERFACES —Module•AddThunk((IUnknown•*)ppvObject• JT (MIConnect i onPoi ntImp V) • riid); CONNECTION POINTS #endif // JVTL.OEBUC.INTERFACES return S 一 OK: } else return E_NOINTERFACE; STDMETHOO(CetConnectionInterface)(IIO,> piid2) { if (piid2 »* NULL) return E.POINTER; •piid2 * *piid; return S_OK; > STDHETHOO(CetConnectionPointContainer)(IConnectionPointContainer*# PPCPC) { T* pT » static«cast(this); // No need to check ppCPC for NULL since QI will do that for us return pT->QueryInterface(IIO_IConnecti onPoi ntContai ner, (void#*)ppCPC); > STOMETHOO(Advise)(IUnknown* pUnkSink. DWORD* pdwCookie); STDMETHOD(Unadvi se)(OWORD dwCookie); STDMETHOO(EnumConnections)(IEnumConnections*# ppEnum); CDV m 一 vec; The _LocCPQueryInterface Method. The -LocCPQuery I nterface method has the same function signature as the COM Querylnterface method but only re­ sponds to requests for IID 一 IUnknown and IID 一 IConnectionPoint by produc­ ing an AddRef’-ed pointer to itself. This makes each base class instance of an IConnecti onPo in t Imp 1 object a unique COM identity. The A ddR ef a n d Release Methods. As usual, you bring in the proper implemen­ tation of these two methods when you define a CComObj ec t class parameterized on your connectable object class, for example, CComObject. The GetConnectionlnterface and GetConnectionPointContainer Methods. The CComEnumConnecti ons typedef declares a class for a standard COM enu­ merator that implements the IEnumConnecti ons interface. You use this class of 392 ATL INTERNALS enumerator to enumerate CONNECTDATA structures, which contain a client sink interface pointer and its associated magic cookie registration token. A template expansion of the ATL CComEnum class provides the implementation. The imple­ mentation of the EnumConnections interface method creates and returns an in­ stance of this enumerator. The CetConnecti onlnterface interface method simply returns the source interface IID for the connection point, so the implementation is trivial. The Cet­ Connecti onPoi ntC ontai ner interface method is also simple, but there is some involved type casting required in order to request the correct interface pointer. The issue is that the current class, this particular IConnectionPointlm pI expansion, doesn’t support the IConnecti onPoi ntContai ner interface. But the design of the template classes requires your connectable object class,repre­ sented by class T in the template, to implement the IConnecti onPoi ntCon­ ta i ner interface. T* pT * static.cast(this); return pT->QueryInterface(IIO_IConnectionPointContainer, (void**)ppCPC): The type cast goes from the connection point subobject down the class hierarchy to the (deriving) connectable object class and calls that class’s Q uerylnterface implementation to obtain the required IConnecti onPoi ntContai ner interface pointer. The Advise, Unadvise,and EnumConnections Methods. The Advi se, Unadvi se, and EnumConnecti ons methods all need a list of active connections. Advi se adds new entries to the list, Unadvi se removes entries from the list, and EnumConnec­ t i ons returns an object that enumerates over the list. This list is of template parameter type CDV. By default, this is type CCom- Dynami cUnkArray, which provides a dynamically growable array implementation of the list. As I described previously, the ATL provides a fixed-size list implemen­ tation and a specialized single-entry list implementation. However, it is relatively easy to provide a custom list implementation because the Advi se, Unadvi se, and EnumConnecti ons implementations always access the list through its well-defined methods, namely, Add, Remove, begi n, end, CetCookie, and CetUnknown.2 2 One reason for a custom implementation could be to keep separate sink lists一 one for each security principal一 and only send certain events to certain sinks based on the principals security clearances. CONNECTION POINTS 393 The Advise Method. The Advi se method retrieves the sink interface IID for this connection point and queries the IUnknown pointer provided by the client for the sink interface. This ensures that the client passes an interface pointer to an object that actually implements the expected sink interface. Failure to provide the correct interface pointer produces a CONN ECT_E_CANNOTCONNECT error. You don’t want to keep a connection to something that can’t receive the callback. Plus, by obtaining the correct interface pointer here, the connection point doesn’t have to query for it during each callback. template 〈class T, const IID* piid, class CDV> STDMETHOOIMP IConnectionPointlmpI::Advise(IUnknown* pUnkSink, DWORD* pdwCookie) { T* pT - static«cast(this); IUnknown* p; HRESULT hRes » S_OK; if (pUnkSink « NULL 11 pdwCookie ** NULL) return E_POINTER; IID iid; CetConnectionlnterfaceW id); hRes ■ pUnkSink->QueryInterface(fid, (void**)&p); if (SUCCEEOED(hRes)) { pT->Lock(); •pdwCookie « m_vec.Add(p); hRes * (*pdwCookie != NULL) ? SJ)K : CONNECT.E^ADVISELIMIT; pT->Unlock(); if (hRes !* S 一 OK) p->Re1ease(); } else if (hRes *= E_NOINTERFACE) hRes * CONNECT_E_CANNOTCONNEa; if (FAILED(hRes)) *pdvyCookie » 0; return hRes; > When the query succeeds, the connection point needs to add the connection to the list. So it acquires a lock on the entire connectable object, adds the connection to the list if there’s room, and releases the lock. 3 9 4 ATL INTERNALS The Unadvise Method. The Unadvi se method is relatively simple. It locks the connectable object, asks the list class to translate the provided magic cookie value into the corresponding IUnknown pointer, removes the connection identified by the cookie, unlocks the connectable object, and releases the held sink interface pointer. template STDMETHODIMP IConnecti onPoi ntImpl::Unadvise(DWORD dwCookie) { T* pT « static.cast(this); pT->Lock(); IUnknown* p » _CDV::CetUnknown(dwCookie); HRESULT hRes = m^vec.Remove(dwCookie) ? SJDK : CONNECT_E_NOCONNECTION; pT->Unlock(); if (hRes — S_OK && p !* NULL) p->Release(); return hRes; The EnumConnedions Method. EnumConnecti on begins with some basic error checking, then creates a new instance of a CComObj ect enumerator on the heap. As before, the ATL enumerator implementation requires that, after instantiation, an enumerator must be initialized from an array, in this par­ ticular case, a contiguous array of CONNECTDATA structures. template STOMETHOOIMP IConnectionPointlmpI::EnumConnectionsC IEnumConnectionsppEnum) { if (ppEnum NULL) return E_POINTER; ♦ppEnum * NULL; CComObject* pEnum » NULL; ATLTRY(pEnum ■ new CComObj ec t ) if (pEnum — NULL) return E_OUTOFMEMORY; T* pT * static_cast(tMs); pT->Lock(); CONNECTDATA* pcd * NULL; CONNECTION POINTS ATLTRYCpcd - new CONNECTDATA[m„vec.end()-m_vec.begin()]) if (pcd ** NULL) { delete pEnum; pT->Un1ock(); return E_OUTOFMEMORY; } CONNECTDATA* pend « pcd; // Copy the valid CONNEaOATAs for (IUnknown** pp * m_vec.begin();ppAddRef(); pend->pUnk * *pp; pend->dwCookie * _COV::CetCookie(pp); pend++; } > // don’t copy the data, but transfer, ownership to it pEnuw->Init(pcd, pend, NULL, AtlFlagTakeOwnership); pT->Un1ock(); HRESULT hRes | pEnum->_InternalQueryInterface(IIO_IEnumConnections, (void**)ppEnum); if (FAILEO(hRes)) delete pEnum; return hRes; } The connection list stores IUnknown pointers. The CONNECTDATA structure contains the interface pointer and its associated magic cookie. EnumConnecti ons allocates space for the initialization array from the heap. It iterates through the con­ nection list entries, copying the interface pointer and its associated cookie to the dynamically allocated CONNECTDATA array. It’s important to note that any non-NULL interface pointers are AddRef’ed. This copy of the interface pointers is going to have a lifetime greater than the EnumConnecti ons method. The call to the enumerator I n it method initializes the instance. It’s critical here to use the A tl FI agTakeOwnershi p argument This informs the enumerator to use the provided array directly, rather than making yet another copy of it. This also means the enumerator is responsible for correctly releasing the elements in the array as well as the array itself. 396 ATL INTERNALS The EnumConnecti ons method now uses .InternalQ uerylnterface to re­ turn an IEnumConnecti ons interface pointer on the enumerator object, which, at this point, is the only outstanding reference to the enumerator.3 Classes Used by an Event Sink First, you need to understand the big picture about event sinks. Your object class may wish to implement multiple event sink Interfaces and/or the same event sink interface multiple times. All event sink interfaces need nearly identical functional­ ity— IUnknown, ID i spatch, Invoke, and looking up the DISPID in a sink map and delegating the event method call to the appropriate event handler. But each imple­ mentation also needs some custom functionality一 specifically, each implementa­ tion must be a unique COM identity. ATL defines a class, 一 ID i spEvent, that implements the common functionality and, through the use of template parameters and interface coloring, allows each derivation from this one C+ + class to maintain a unique COM identity. This means ATL implements all specialized event sink implementations using a single C+ + class, 一 IDi spEvent. The JDispEvent Class Let’s examine the _ID i spEvent class. The first interesting aspect is that it is in­ tended to be used as an abstract base class. The first three virtual methods are de­ clared using the COM standard calling convention and are all pure virtual. The first method is _LocDEQueryInterface, and the following two are the AddRef and Rel ease methods. This gives the _ID i spEvent class the vtable of a COM object supporting the IUnknown interface. The _ ID i spEvent class cannot simply derive from IUnknown because it needs to provide a specialized version of Query In te r ­ face. A derived class will need to supply the 一 LocDEQuery I nterface, AddRef, and Release methods. class ATL_NO_VTABLE _IDi spEvent { public: //this method needs a different name than Querylnterface STOMETHOO(_LocOEQueryInterface)(REFIIO riid, void ** ppvObject) « 0 ; virtual ULONG STOMETHOOCALLTYPE AddRef(void) * 0; virtual ULONC STDMETHODCAILTYPE Release(void) = 0; 3EnumConnections uses —InternalQuerylnterface rather than IUnknown::QueryInterface because the latter results in 压 virtual function call, whereas the former is ji more efficient direct CONNECTION POINTS The class maintains five member variables, only one of which is always used, m_dwEventCookie, which is the only member variable initialized by the con­ structor. _IDispEvent() {m—dwEventCookie * OxFEFEFEFE;} The m_dwEventCooki e variable holds the connection point registration value re­ turned from the source object’s IC onnecti onPoi n t :: Advi se method. It’s needed to break the connection. The class assumes that no event source will ever use the value OxFEFEFEFE as the connection cookie because it uses that value as a flag to indicate that no connection is established.4 While highly unlikely, this event is not impossible. The m_l i b i d, m_J i d, m一 wMajorVerNum,and m一 wMi norVerNum variables hold the type library GUID, the source interface IID, and the type library msyor and minor version number, respectively. CUID m.libid; 110 n U id ; unsigned short m_wMajorVerNum; unsigned short norVerNum; DWORD m_dwEventCooki e; This 一 IDispEvent class provides the Di spEventAdvi se and DispEvent- Unadvi se methods, which establish and break, respectively, a connection between the specified source object’s (pUnk) source interface (pi i d) and the _Di spEvent sink object. HRESULT Di spEventAdvi seCIUnknown* pUnk# const IID* piid) { ATLASSERTCm.dwEventCookie — OxFEFEFEFE); return AtlAdvise(pUnk, (IUnknown*)thisf *piid, Aw^dwEventCookie); } HRESULT Disp£ventUnadvise(IUnknown* pUnk, const IID* piid) 4 Note that this places a constraint on a connection list implementation (that is, the COV template pa­ rameter class), namely, that it never provide the value OxFEFEFEFE as a connection cookie. Of course, the _ ID i spEvent class could maintain a separate “connection established" flag, but that would increase the size of a class instance. Minimizing instance size doesn't seem to be a concern in the _ID i spEvent class, though, because the class contains four other member variables that aren’t always used. Typically, the approach in ATL is to factor out into a separate, derived class any member variables that aren't always needed in its base class. 3 9 8 ATL INTERNALS HRESULT hr ■ AtlUnadvise(pUnk, *piid, nudwEventCookie): m.dwEventCookie • OxFEFEFEFE; return hr; > You can implement multiple event sinks in a single ATL COM object. In the most general case, this means you need a unique event sink for each different source of events (source identifier). Further, this means you need a unique event sink object for each separate connection (source interface) to a source of events. The JDispEventLocator Class Implementing multiple event sinks requires the sink to derive indirectly from 一 ID i spEvent multiple times. But we need to do so in a way that allows us to find a particular _ID i spEvent base class instance, given a source object identifier and the source interface on that object. ATL uses the template class 一 IDispEventLocator to do this. Each unique _ID i spEventLocator template invocation produces a different, addressable _ID i spEvent event sink instance.5 template class ATL_NO_VTABLE ..IDispEventLocator : public _I0ispEvent { public: The 一IDispEventSimplelmpI Class The IDispEventSimplelmpI class implements the IDi spatch interface. It de­ rives from the 一 ID i spEventLocator class to inherit the IUnknown vtable, the member variables, and the connection point Advi se and Unadvi se sup­ port provided by the _ ID i spEvent base class. template class ATtJ^O.VTABLE IDispEventSimplelmpI : public _IDispEventLocator 5 Keith Brown pointed out that it would have been more appropriate to caJl these ^Locator" classes XOMIdentity" classes, for example, _IDi spEventCOMIdenti ty and IConnectionPointCOHIden- t i ty. Their fundamental purpose is to provide a unique base class instance for each required COM iden­ tity. Yes, you need to locate the appropriate base class when needed, but the sole purpose in renaming the Querylnterface method is to implement a separate identity. CONNECTION POINTS // Abbreviated for clarity STOMETHOOCLocDEQueryInterface)(REFIID riid, void ** ppvObject); virtual ULONC STDMETHOOCALLTYPE AddRef(); virtual ULONC STDMETHOOCALLTYPE Release(); STDMETHOD(CetTypeInfoCount)(UINT* pctinfo); STDMETHOO(CetTypelnfo)(UINT itinfo, LCID lcid, ITypelnfo** pptinfo); STDMETHOO(CetlDsOfNames)(REFIIO riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid); STDMETHOO(Invoke)(DISPID dispidMember, REFIIO riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr); Notice that the ID i spEventSi mpl elmpl class provides an implementation of the 一LocDEQuery Interface ,AddRef, and Rel ease methods that it inherited from its —ID i spEvent base class. Notice as well that the next four virtual methods are the standard IDi spatch interface methods. The IDi spEventSi mpl elmpl class now has the proper vtable to support ID i spatch. It cannot simply derive from ID i s- patch to obtain the vtable because the class needs to provide a specialized version ofQuerylnterface. The ID i spEventSi mpl elmpl class implements the _LocDEQueryInterf ace method such that each event sink is a separate COM identity from that of the deriv­ ing class. The event sink object is supposed to respond positively to requests for its source dispatch interface ID, the IUnknown interface, the ID ispatch interface, and the GUID contained in the m_i i d member variable. But the ATL 3.0 version of the code has a few bugs in it. STOMETHOO(.LocDEQueryInterface)(REFIID riid. void ** ppvObject) { if (IniincIs£qualCUIO(riid, *pdiid) II InlinelsEqualUnknown(riid) i| IniineIs£qua1CUI0(riid, IID_IDispatch) |) InlineIsEqualCUID(riidf m_iid)) { 1f (ppvObject *= NULL) return E^POINTER; ♦ppvObject 職 this: AddRef(); #ifdef JVTL^DEBUC.INTERFACES «Module.AddThunk((IUnknown**)ppvObject, _T("I0ispEventlmpl")• riid); ATL INTERNALS #endif // ^ATLJ)EBU The first problem is that when you are using the ID i spEventSi mpl elmpl class di­ rectly, the m_i i d member variable is never initialized. Fortunately, the sink is com­ monly asked only for its source dispatch interface ID (the *pdi i d value), its IUnknown interface (by the remoting layer), or its ID i spatch interface (by script­ ing languages). In these cases the buggy code (the last In i i nelsEqualGUID call) never gets executed. However, when a query isn’t for one of these three interface Gl ITDs, the event sink will hand out its interface pointer in response to some random GUID. In Debug builds, uninitialized memory is set, by default, to the bit pattern OxCD. For heap allocated instances in debug builds, the _LocDEQue ry ln te r f ace method re> sponds to queries for the GUID {CDCDCDCD-CDCD-CDCD-CDCD-CDCDCDCDCDCD}. In release builds, it responds to a random GUID. The second problem is that you can use the ID i spEventlm pl class (discussed shortly) without having to specify the source dispatch interface GUID template pa­ rameter. The template provides a default value of I ID 一 NULL Composite controls frequently do this. The i i i d parameter is then initialized from the type library to the 即 propriate default source interface GUID. But the code responds to queries for IID 一 NULL (*pdi i d) when it shouldn't The ID i spEventSi mpl elmpl class also provides a simple implementation of the AddRef and Rel ease methods. This permits the class to be used directly as a COM object. virtual ULONG STOMETHOOCALLTYPE AddRef() { return 1; } virtual ULONC STOMETHOOCALLTYPE ReleaseO { return 1; } However, when you compose the class into a more complex ATL-based COM ob­ ject, the AddRef and Rel ease methods in the deriving class will be used. In other words, an AddRef to the event sink of a typical A IL COM object will call the deriv­ ing object’s CComObject:: AddRef method (or whatever your most-derived class is). Watch out for reference-counting cycles due to this. A client holds a reference to the event source, which holds a reference to (nominally) the event sink but which is actually to the client itself. The IDi spEventSi mpl elmpl class implements the CetTypelnfoCount, CetTypelnfo, and CetlDsOfNames methods by returning the error E^NOTIMPL. CONNECTION POINTS An event dispatch interface is only required to support the IUnknown methods and the Invoke method. STDHETHOO(CetTypelnfoCount)(UINT* petinfo) {return E—NOTIMPL;} STDMETHOO(CetTypelnfo)(UINT itinfo, LCID lcid, ITypelnfo** pptinfo) {return E.NOTIMPL;} STDMETHOO(GetlOsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid) {return 匕 NOTIMPL;} STOMETHOOCInvoke)(OISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparamsr VARIANT* pvar Result, EXCEPINFO* pexcepinfo, UINT* puArgErr); The Invoke method searches the deriving class's event sink map for the 叩 propri- ate event handler for the current event It finds the appropriate sink map by calling _GetSi nkMap, which is a static member function defined in the deriving class by the BECIN_SINK_MAP macro (described later in this section). The proper event handler is the entry that has the matching event source ID (nID) as the template in­ vocation, the same source interface IID (pdi id ) as the template invocation, and the same DISPID as the argument to Invoke. When the matching event sink entry specifies an __ATL一 FUNC__INFO structure (meaning the event sink entry was defined using the SINK_ENTRY_INFO macro), Invoke uses the structure to call the handler. Otherwise, Invoke calls the Get- FuncInfoFromld virtual function to obtain the required structure. When the Get- FuncInfoFromld function fails, Invoke silently returns S_OK. This is as it should be because an event handler must respond with S—OK to events the handler doesn’t r e c o ^ ^ H ust override the Get FuncInfoFromld method when using the SINK. ENTRY_EX macro with the ID i spEventSi mpl elmpl class. The default implemen­ tation silently fails: virtual HRESULT CetFuncInfoFromId(const IID& iid, { return E__NOTIMPL: } 402 AT L INTERNALS But this does mean that if you use the ID i spEventSi mpl elmp l class directly, and you specify an event handler using the SINK一 ENTRY—EX macro, and you forget to override the GetFuncInfoFromld mothoii ur implement it incorrectly, everything compiles cleanly but your event handler tv ill never be called. The ID i spEventSi mpl elmpl class provides some overloaded helper methods for establishing and breaking a connection to ari event source. The following two methods establish and break a connection between the current sink and the specified event interface (pi i d) on the specified event source (pUnk). //Helpers for sinking events on random IUnknown* HRESULT Di spEventAdvise(IUnknown* pUnk, const IID* piid) { ATLASSERT(nudwEventCookie OxFEFEFEFE); return At1Advise(pUnkt (IUnknown*)this, *piid, Am^dwEventCookie); } HRESULT DispEventUnadvise(IUnknown* pUnk, const IID* piid) { HRESULT hr * AtlUnadviseCpUhk, m_dwEventCook*ic); m^dwEventCookie * OxFEFEFEFE; return hr; } The next two methods establish and break a connection between the current sink and the specified event source using the sink’s dispatch interface HRESULT Di SpEventAdvise(IUnknown* pUnk) { return JtDi spEvent::Di spEventAdvi se(pUnk, pdiid); > HRESULT Di spEventUnadvi se(IUnknown* pUnk) { return _I0i spEvent::Di spEventUnadvi se(pUnk, pdiid); > The Sink Map: Associated Structure, Macros, and the -GetSinkMap Method The sink map is an array of _ATL_EVENT一ENTRY structures. The structure contains the following fields: nControl ID The event source identifier; control ID for contained controls piid The source dispatch interface IID CONNECTION POINTS 403 d is p id pfn p ln fo nO ffset The offset of the event sink implementation from the deriving class The event callback dispatch ID The member function pointer of the event handler to invoke The _ATL_FUNC_INF0 structure used for the event handler call template struct ^TL.EVENT^ENTRY UINT nControlIO const IID* piid int nOffset; //ID identifying object instance //dispinterface IID DISPID dispid; void (— stdcal1 _ATL.FUNC.INFO* //offset of dispinterface from this pointer //DISPID of method/property T ::*pfn)(); //method to invoke plnfo; //pointer to info structure When you use the BEGIN_SINK_MAP macro, you define a static member func­ tion in your class called _G etSi nkMap. It returns the address of the array of _ATL 一 EVENT_ENTRY structures. ^define BECIN-SINK_MAPCclass)\ static const _ATL-EVENT_ENTRY<_class># _CetSinkMap()\ {\ typedef _class _at1.event^c1asstype;\ static const _ATL«EVENT.ENTRY<_c1ass> map[] ® { Each SINK_ENTRY」CNFO macro adds one _ATL_EVENT_ENTRY structure to the fdefine SINICENTRY_INFO(id, iid, dispid, fn, info) \ {id. &iid, (int)(static.cast<_IDi spEventLocator base class with respect to your 404 ATL INTERNALS deriving class (the class containing the sink map). This allows us to find the appro­ priate event sink referenced by the sink map entry. (int)Cstatic-cast<一 IDispEventLocator*> (_atl_event_c1asstype*)8))-8 The following cast saves the event handler function address as a pointer to a mem­ ber function. (void ( _ stdcal! _at1_event_c1asstype::*)(» fn The SINK_ENTRY—EX macro is the same as the SINK_ENTRY_INFO macro with a NULL pointer for the function information structure. The SINK_ENTRY macro is the same as the SINK_ENTRV_INFO macro with IID_NULL for the dispatch inter­ face and a NULL pointer for the function information structure. #define SIN»e.ENTRY_EX(idf iid, dispid, fn)\ SINK^ENTRY^INFOdd, iid, dispid, fn, NULL) 参 define SINK^ENTRY(id, dispid. fn)\ SINK.ENTRY^EX(1d, IIO^NULL, dispid, fn) The END_SINK_MAP macro ends the array and completes the 一 GetSinkMap function implementation. 參 define ENO.SINICMAPO {0, NULL, 0, 0, NULL, NULL} }; return nap;} The JDispEventlmpl Class Finally, we come to the ID i spEventlmpI class. This is the class used by the code generation wizards. It derives from the ID i spEventSi mpl elmpl class and there­ fore inherits all the functionality previously discussed. The additional template pa­ rameters specify a type library that describes the source dispatch interface for the event sink. template class ATL_NO.VTABLE IDi spEventlmpI •• public IDispEventSimplelmpl CONNECTION POINTS The main feature of the ID i spEventlm pl class is that it uses the type infor­ mation to implement functionality missing in the base class. The class implements the GetTypelnfoCount, GetTypelnfo, and GetlDsOfNames methods using the type library via a CComTypelnfoHolder object. 5T0METH00(GetTypelnfoCount)(UINT* pctinfo) {*pctinfo « 1; return S_OK;} STOMETHOO(GetTypelnfo)(UINT itinfo. LCIO lcid, ITypelnfo** pptinfo) {return _tih.CetTypeInfo(itinfo. lcid, pptinfo);} STOMETHODCCetlDsOfNames)(REFIIO riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid) {return _tih.GetIOsOfNames(riid, rgszNames, cNames, lcid, rgdispid);} It also overrides the GetFuncInfoFromld method and initializes an _ATI__FUNC _INFO structure using the information provided m the type library //Helper for finding the function index for a DISPID HRESULT CetFuncInfoFromldCconst IXD& /MidV, DISPID dispidMember, LCIO lcid, JVTL.FUNCJNFOA info); Summary The connection point protocol defines a mechanism for a client interested in re­ ceiving event callbacks to pass its event sink interface pointer to an event source. Neither the client nor the event source needs to be written with knowledge of each other. Objects hosted on a web page or, more generally, objects used by scripting languages, must use the connection points protocol to fire events to the scripting engine. Also, ActiveX controls fire their events using the connection points proto­ col. While this protocol is acceptablo for intra-apartment use, it is inefficient (when considering round-txips) for use across an apartment boundary. ATL provides the ID i spEvent and ID i spEventsSi mpl e classes for a client object to use to receive event callbacks. Additionally, ATL provides the Connection Points Proxy Generator so you can easily generate a class that manages a connec­ tion point and contains helper methods to fire the events to all connected clients. CHAPTER 9 Windowing A T L is not simply a set of wrapper classes for COM. In the same style, it also wraps the section of the Win32 API relating to creating and manipulating windows, dia­ logs, and Windows controls. In addition to basic support to remove the drudgery of Windows programming, the ATL windowing classes include such advanced features as subclassing and superclassing. Further, this window support forms the basis for both COM controls and COM control containment, covered in the following chapters. The Structure of a Windows Application A standard Windows application consists of several well-known elements, as follows: ■ The entry point, _tW i nMai n,which provides the HINSTANCE of the application, the command-line arguments, and the flag indicating how to show the main window. _ A call to Regi sterC l ass to register the main window class. ■ A call to CreateW i ndow to create the main window. ■ A calJ to ShowWi ndow and UpdateWi ndow to show the main window. ■ A message loop to dispatch messages. ■ A procedure to handle the main window's messages. ■ A set of message handlers for messages the window is interested in handling. ■ A call to DefWi ndowProc to let Windows handle messages we're not inter­ ested in. ■ A call to PostQui tMessage once the main window has been destroyed. A bare-bones example follows: #inc1ude ”stdafx.h" // Includes windows.h and tchar.h LRESULT CALLBACK WndProc(HWND, UINT, WPARA.M, LPARAM); 407 408 ATL INTERNALS // Entry point int APIENTRY _1 HINSTANCE LPTSTR "hi nstPrev*/, pszCmdLine, nCmdShow) wc // Register the main window class LPCTSTR pszMainWndClass = 一 T("Wi ndowsApp"); { sizeof(WNDCLASSEX) }; CS.HREDRAW | CS_VREDflAW; hinst; Loadlcon(0, IDIJXPPLICATION); LoadCursorCO, IDC_ARR0W); (HBRUSH)(COLOR_WINDOW+1); pszMainWndClass; WndProc; WNDCLASSEX wc.style wc.hlnstance wc.hlcon wc.hCursor wc.hbrBackground wc.IpszClassName wc.lpfnWndProc if( !RegisterClassEx(Awc) ) return -: // Create the main window HWND hwnd - CreateWindowExCWS_EK_CLIENTEDCE, pszMainWndClass, _ T("Windows Application"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 0 , 0, hinst, 0); if( !hwnd ) return -1; // Show the main wi ndow ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // Main message loop MSG msg; whi1e( CetMessage(&msg, 0, 0, 0) ) { Trans1ateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; > WINDOWING All Windows applications have similar requirements. These requirements can be expressed in procedural Win32 calls, as the example just showed. However, when procedural calls model an underlying object model, C+ + programmers feel compelled to wrap those calls behind member functions. The windowing part of the Win32 API (often called User32) is clearly modeling an underlying object model consisting of window classes (represented by the WN DC LAS SEX structure), window objects (represented by the HWND), and member function invocation (represented by calls to the WndProc). For C + + programmers averse to the schism be­ tween their preferred object model and that of User32, ATL provides a small set of windowing classes, as shown in Figure 9.1. The classes in bold, CWindow, // Windows procedure LRESULT CALLBACK WndProc(HWND hwnd, UINT nMsg, WPARAM wparam, LPARAM 1 param) switch( nMsg ) { // Message handlers for messages we're interested in case WM—PAINT: { PAINTSTRUCT ps ; HDC hdc = BeginPaint(hwnd, &ps); RECT 「ect; CetClientRect(hwnd, &rect); DrawTextChdc, 一 TCHello, Windows") , -1,&rect r DT—CENTER 丨 DT_VCENTER | DT—SINGLELINE); EndPaint(hwnd, &ps); } break; // Post the quit message when main window is destroyed case WM 一 DESTROY: PostQuitMessage(O); break; // Let Windows handle message we don't want default: return DefWindowProc(hwnd, nMsg, wparam, 1 param); break; } 410 ATL INTERNALS CWInctow CWirKJowImp CWIndowtf Figure 9.1. Unified Modeling Language diagram of ATL window classes CContainedWlndowT “Base npl OVindowImpl, CWinTraits, CWinTraitsOR, CDialoglmpl, CSimpleDialog, and C C ontai nedWi ndowT, are the most important. The others, CWi ndowlmpl - Root, CWi ndowlmpl BaseT, and CDi ai oglmpl BaseT, are helper classes to sepa­ rate parameterized code from invariant code. This separation helps to reduce template-related code bloat, but these classes are not a fundamental part of the ATL windowing classes. The former classes will form the discussion for the bulk o[ the rest of this chapter. CWindow An HWND Wrapper The most basic of the windowing classes in ATL is CWi ndow. Its chief job is to hold an HWND, which it can obtain via several member ftinctions: class CWindow { publi c: CMIndowCHMND hWnd • NULL) { mJrMnd • hWnd; } 5 CWindowA op«rator«(HWN0 hWnd) { BL.hWnd • hWnd; return > WINDOWING void Attach(HWND hWndNew) { m 一 hWnd * hWndNew; } HWND CreateCLPCTSTR IpstrWndClass, HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName * NULL, OWORD dwStyle * 0, DWORD dwExStyle = 0, UINT nID « 0, LPVOIO lpCreateParam * NULL) { // Calls CreateWindowEx and caches result in m_hWnd return nuhWnd; } HWND CreateCLPCTSTR IpstrWndClass, HWND hWndParent, LPRECT IpRect * NULL, LPCTSTR szWindowName « NULL, OWORD dwStyle *» 0, DWORD dwExStyle » 0, HHENU hMenu = NULL, LPVOIO lpCreateParam « NULL) { // Calls CreateWindowEx and caches result in m—hWnd return m„hWnd; } The HWND itself is available either as a public data member or via the HWND type­ cast operator: class CWindow { public: HWND iiJiWnd; operator HNNOO const { return m_hWnd; } If you’d like to clear the HWND, you can set the nuhWnd manually or use the De­ tach member function: inline HWND CWindow::Detach() { HWND hWnd • «L.hWnd; nL.hWnd * NULL; return hWnd; 412 ATL INTERNALS A CWi ndow object represents a wrapper around the HWND, not the window itself. The CWindow destructor will not destroy the underlying window. Hence, there's really no need to ever call Detach. HWND Wrapper Functions Once the CWi ndow object has an HWND, you can make use of the rest of the CWi n- dow class member functions. The purpose of CWi ndow is to act as a wrapper for all the functions of the User32 API. For every function that takes an HWND as the first argument, the CWi ndow class has a corresponding member function that uses the cached m_hWnd. For example, instead of calling SetWi ndowText, void SayHello(HWNO hwnd) { SetWi ndowText (hwnd, ___TCHello")); > you would use the SetWi ndowText member function: void SayHello(HWND hwnd) { CWindow wnd = hwnd; wnd.SetWindowText(__T("Hello")); } When I said ail the User32 functions that take an HWND as a first parameter, 】meant all. As near as I can tell, with the exception of one function (Set Fore- groundWi ndow), the entire windowing API is represented as a member function of CWi ndow. The CWi ndow class declaration comments break the wrapped functions down into several categories Alert functions Clipboard functions Font functions Icon functions Miscellaneous operations Update and painting functions Window state functions Attributes Coordinate mapping functions Help functions Menu functions Scrolling functions Window access functions Window text functions Caret functions Dialog box item functions Hot key functions Message functions Timer functions Window size and position functions Window tree access WINDOWING HWND Helper Functions The vast injy'ority of the CWi ndow member functions are merely inline wrappers on the raw functions. This means you get the syntactic convenience of member func­ tions without any additional runtime overhead. In addition, there are several helper functions that encapsulate common functionality that we often end up writing time and again above and beyond straight wrappers: class CWi ndow { _ » • DWORD CetStyleC) const; DWORD GetExSty1e() const; BOOL HodifyStyle(DWORD dwRemove, DWORD dwAdd, UINT nFlags * 0); BOOL HodifyStylcEx(DWORD dwRemove, DWORD dwAdd, UINT nFlags * 0); BOOL Resizedient(int nWidth, int nHeightf BOOL bRedraw « TRUE); HWND GetOescendantWindow(int nIO) const; BOOL CenterWindow(HWND hWndCenter * NULL); BOOL GetWindowText(BSTR* pbstrText); BOOL CctWindowT«xt(BSTR& bstrText); HWND GetTopLevelParent() const; HWND GetTopLevelWindow() const; Likewise, CWi ndow provides a number of type-safe wrappers for calling Send- Message for common messages, performing the error-prone casting chores for us: class CWi ndow { • * • void SetFontCHFONT hFont, BOOL bRedraw » TRUE); HFONT GetFont() const; void Print(HDC HOC, DWORD dwFlags) const; void PrintClient(HDC hDC, DWORD dwFlags) const; void SetRedraw(BOOL bRedraw « TRUE); HICON SetIcon(HICON hlcon, BOOL bBiglcon » TRUE); HICON C«tIcon(B00L bBiglcon * TRUE) const; int SetHotK€y(W0RD wVirtualKeyCode, WORD wModifiers); DWORD GetHotKeyO const; void NextOlgCtrl() const; void PrevDlgCtrl() const; 414 ATL INTERNALS void GotoDlgCtrl(HWND hWndCtrl) const; void SendMessageToOescendants(UINT message, WPARAM wParam » 0, LPARAM IParam * 0, BOOL bOeep * TRUE); Using CWi ndow Before we can put the CWi ndow class to use in our sample Windows application, we have to establish support for the ATL window classes in our Win32 application. The first step is to add the following lines to the precompiled header (called s td a fx . h): II Needed to use ATL windowing classes #include extern CComModule __Module; ♦include The ATL windowing classes are defined in a tl w in. h, but before we can include that, we have two responsibilities. The first is to include a tl base • h, which a t l - w in.h depends on. The second is to declare an instance of ai CComModule called _Modul e, just like an ATL server. Very much in the same way that all MFC programs have a single global instance of the CWi nApp class, all ATL windowing applications must have a single global instance of a module called _Modul e. The module’s job in a windowing application is mostly to hold onto the application’s HINSTANCE, which we must remember to provide in a call to the CComModul e member function In it . The following modified .tW inM ain implementation shows the initialization and termination of the module as well as using a CWi ndow instead of an HWND: #inc1ude "stdafx.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // The sin CConModule ingle, global ATL aodule // Entry point int APIENTRY _tWinMain(HINSTANCE hinst, HINSTANCE /*hinstPrev*/, LPTSTR pszCmdLine, int nCmdShow) { WINDOWING 415 // Initialize the ATL module .„Modu1e.Init(0, hinst); /./ Register the main window class // Create the main window CWindow wnd; wnd.CreateCpszMainWndClass, 0, CWindow::rcDefault, —TC'Windows Application"), WS_OVERUPPEDWINDOW, WS_EX_CLIENTEDCE); if( !wnd ) return -1; // Show the main window wnd.CenterWindow(); wnd.ShowWindow CnCmdShow); wnd.UpdateWindow(); // Main message 'oop // Shut down the ATL module 一Module.TermO; return msg.wParam; } Notice that the structure of the program remains the same. The only difference is that we re calling member functions instead of global functions. The WndProc can be similariy up(iat.ed: LRESULT CALLBACK WndProc(HWND hwnd, UINT nMsg, WPARAM wparam, LPARAM 1param) { switch( nMsg ) { // WM一 PAINT handler case WM_PAINT: { PAINTSTRUCT ps ; □Window wnd(hwnd); HDC hdc = wnd.Begi nPaint C&ps); ATL INTERNALS RECT rect; wnd.CetCli entRect(Arect); DrawText(hdc, 一 T("Hellof Windows"), -1, Arect, DT_CENTER 丨 DT—VCENTER 丨 D乙 SINGLELINE); wnd.EndPaintC&ps); } break; …II The rest is the same return 0; CWi ndow is a step in the right direction. Instead of calling global functions and passing a handle, we’re now able to call member functions on an object. However, we’re still registering a Windows class instead of creating a C++ class and we,re still handling callbacks via a WndProc instead of via member functions. To com­ pletely fulfill our desires, we need the next most important class in the ATL win­ dowing hierarchy, CWi ndowlmpl • CWindowlmpI The Window Class The CWi ndowlmpl class derives ultimately from CWi ndow and provides two addi­ tional features, window class registration and message handling. I’ll discuss mes­ sage handling after we explore how CWindowlmpI manages the window class. First, notice that, unlike CWi ndow, the CWindowlmpI member function Create doesn’t take the name of a window class: template class ATL 一 NO 一 VTA8LE CWi ndowlmpl : public CWi ndowlmplBaseT < TBase, TWinTraits > { public: OECURE.WNO.CLASSCNULL) HWND Create(HWND hWndParent. RECTA rcPos, LPCTSTR szWindowName » NULL, Wl NDOWI NG DWORD dwStyle • 0. DWORD dwExStyle * 0. UINT nIO * 0, LPVOID lpCreateParam • NULL); Instead of passing the name of the window class, the name of the window class is provided in the DECLARE_WND_CLASS macro. A value of NULL will cause ATL to generate a window class of the form ATL<8-digit number>. We could declare a CWi ndowlmpl -based class using the same window class name we registered us­ ing Regi sterC I ass. However, that’s not necessary. It’s far more convenient to let ATL register the window class the first time we call Create on an instance of our CWi ndowlmpl -derived class. This initial window class registration is done in the implementation of CWi ndowlmpl:: Create: HWND CWindoMlapI::Create(HWNO hWndParent, RECT& rcPos, // Generate a class nane if one is not provided if (T::CetWndC1assInfo().m_lpszOrigName •« NULL) T ::GetWndC1assInfo().m_lpszOrigName - GetWndClassName(); // Register the Mindow class if it hasn't aiready been registered ATOM atom * T::CetWndClassInfo().Register(Ara^pfnSuperWindowProc); Assuming a class derived from CWi ndowlmpl,our _tWi nMai n has gotten much smaller class 04a1nW1ndow : public O lindowlnpl // Entry point int APIENTRY _tWinMain(HINSTANCE hinst, HINSTANCE /*hi nstPrev*/, LPTSTR pszCmdLine, LPCTST"zWindowName - NULL, DWORD tyle « 0, DWORD dwExStyle - 0. UINT nID » 0, LPVOID lpCreateParam * NULL) { // Initialize the ATL module 418 ATL INTERNALS // Create the aain window CMainWindow wnd; wnd.Create(0, CWindow::rcDefault, —TC'Windows Application")* WS.OVERUPPEDWINOOW v WS_EX.CLIENTEDCE); ifC 丨wnd ) return -1; II Show the main window, run the message loop, shut down the module return msg.wParam; Modifying the Window Class, Each CWi ndowlmpl class maintains a static data structure called a CWndClass- In fo , which is a type definition for either an _ATL_WNDCLASSINFOA or an _ATL _WNDCLASSINFOW structure, depending on whether you’re doing a Unicode build or n o t The ANSI version is shown here: struct ^ATU.WWXXASSINFOA ICLASSEXA IWNDCLASSEXA m j/tci LPCSTR m_lpszOrigName; WNDPROC pWndProc; LPCSTR mJIpszCursorlD; m_bSystemCursor; n_ato«; CHAR m_szAutoName[13]; ATOM Register(WNDPROC* p) { return AtlModuleRegisterWndClassInfoA(&_>todule, this, p); } BOOL ATOM The most important members of this structure are m_wc and m_atom. The m_wc member represents the window class structure, that is, what you would use to reg­ ister a class if you were doing it by hand. The m_atom member is used to determine if the class has been already been registered or not This is usefu) if you’d like to make changes to m_wc before the class has been registered. Each class derived from CWi ndowlmpl gets an instance of CWndCl a ssln fo in the base class from the use of the DECLARE_WND_CLASS macro, defined as follows: #define DECLARE.WHD.CLASS(WndC1assName) \ static CWndClassInfo& CetWndCIassInfoO { \ static CWndClasslnfo wc * { \ WINDOWING { si zeof (WNDCLASSEX), CS—HREDRAW 丨 CS—VREDRAW | CSJ)8LaKS, \ StartWindowProc. 0, 0, NULL, NULL, NULL, \ (HBRUSH)(COLOR.WINDOW + 1)t NULL, WndClassName, NULL \ NULL, NULL, IDCLARROW, TRUE, 0, JT(H") \ return wc; \ } This macro defines a function called GetWndCl assln fo and initializes the values to commonly used defaults. If you’d like to also specify the class style and the back­ ground color, you can use another macro called DECLARE_WND_CLASS_EX: #define OECLARE^WND.CLASS.EX(WndClassName. styU, bkgnd) \ static CWndClassInfo& CetWndClassInfoO { \ static CWndCIasslnfo wc - { \ { sizeof(WNDCLASSEX), style, StartWindowProc, \ 0, 0, NULL, NULL, NULL. (HBRUSH)(bkgnd ♦ 1), NULL, WndClassName, \ NULL \ NULL, NULL, IDCARR0W, TRUE, 0, JT("") \ return wc; \ However, neither macro provides enough flexibility to set all the window class information you’d like, for example, large and small icons, cursor, or background brush. While it’s possible to define an entire set of macros in the same vein as DECLARE_WND_CLASS, the combinations of what you’d like to set and what you’d like to leave as a default value quickly get out of hand. Frankly, it’s easier to modify the CWndCl a sslnfo structure directly using the GetWndCl a ssln fo function. The CWi ndowlmpl -derived class’s constructor is a good place to do that, using the m_atom variable to determine if the window class has already been registered. For example: CMainWindowO { // Retrieve the window class inforwation CWndClassInfoA wci * CetWndClassInfoO; // If the wc hasn't already been registered, update it if( !wci.M_atoM ) { ATL INTERNALS wci.m_wc.hlcon = LoadIcon(_Module.CetResourceInstance(), MAKEINTRESOURCE(IDI.BARFBONES)); wci.m_wc.hlconSm = (HICON)::LoadImage(_Module.CetResource- I n s t a n c e O , MAKEINTRESOURCECIDI.BAREBONES) , IMAGE一 ICON, 16 , 16, LR_DEFAULTCOLOR) ; wci.m_wc.hbrBackground = CreateHatchBrush(HS_DIAGCROSS, RGB(0, 0 , 255)); Setting the WN DC LAS SEX member directly will work for most of the members of the m一 wc member of CWndCl a s s ln fo . However, for some reason, the ATL team de­ cided to treat cursors differently. For cursors, the CWndCl assln fo structure has two members, m_l p szC u rso rlD and m」)Sy stemCu rso r, that will be used to over­ ride whatever is set in the hCursor member of m_wc. For example, to set a cursor from the available system cursors, you must do the following: // Can't do this: II wci.m_wc.hCursor = LoadCursor(0, IDC_CR0SS); // Must do this: wci.m_bSystemCursor = TRUE; wci.m_lpszCursorID = IDC_CR0SS ; Likewise, to load a custom cursor, the following is required: II Can't do this: // wci.m_wc.hCursor = LoadCursor(_Module.GetResourcelnstanceO, // MAKEINTRESOURCECIDC.BAREBONES)); // Must do this: wci.m_bSystemCursor = FALSE; wci.m_lpszCursorlD = MAKEINTRESOURCE(IDC_BAREBONES); Remember to keep this special treatment of cursors in mind when creating CWi n - dowltnpl-derived classes with custom cursors. WINDOWING 421 Window Traits In the same way that an icon and a cursor are coupled with a window class, often it makes sense for the styles and the extended styles to be coupled as well. For ex­ ample, frame windows have different styles than child windows. When we develop a window class, we typically know how it’s going to be used; for example, our CMai n- Wi ndow class is going to be used as a frame window and WS—OVER LAPP EDWIN DOW is going to be part of the styles for every instance of CMai nWi ndow. Unfortunately, there is no way to set default styles for a window class in the Win32 API. Instead, the window styles have to be specified in every call to CreateW i ndowEx. To allow de­ fault styles and extended styles to be coupled with a window class, ATL allows you to group styles together and reuse them in an instance of the CWi nT ra i t class: template <0W0RD t.dwStyle * 0, DWORD t^dwExStyle » 0> class CW1nTr«1ts { public: static DW0R0 CetWndStyle(DW0R0 dwStyle) { return dwStyle »« 0 ? t^dwStyle : dwStyle: } static DWORD CetWndExStyle(DWORD dwExStyle) { return dwExStyle 0 ? t.dwExStyle : dwExStyle; } As you can see, the CWi nT ra i t class holds a set of styles and extended styles. When combined with a style or an extended style DWORD, it will hand out the passed DWORD if it is nonzero; otherwise, it will hand out its own value. For example, to bundle my9nonze irred st:preferred styles together into a window trait, I would do the following: typedef CWinTraits CMai nWinTraits; A window traits class can be associated with a CWi ndowlmpl by passing it a template parameter class CMai nWi ndow : public CWi ndowlmpl Now, when creating instances of a CWi ndowlmpl-derived class, I can be explicit about what parameters I want, or, by passing 0 for the style and/or the extended style, I can get the window traits style associated with the class. For example: ATL INTERNALS II Use the default value of 0 for the style and the II extended style to get the window traits for this class, wnd.Create(0, CWi ndow::rcDefault, —TC'Windows Application")); Because I’ve used a CWi nTrai t class to group together related styles and extended styles, if I need to change a style in a trait, the change will be propagated to all in­ stances of any class that uses that trait. This saves me from finding the instances and manually changing them one at a time. For the three most common kinds of windows— frame windows, child windows, and MDI child windows—ATL comes with three built-in window traits classes: typedef CWinTraits CControlWInTrai ts; typedef CWinTraits CFraweWinTraits; typedef CWi nTrai ts CNDIChildWlnTraits; If you’d like to leverage the styles of an existing window traits class but add styles, you can use the CWi ndowTrai tsOR class: template <0W0RD idwStyle « 0, DWORD t一 dwExStyle « 0» class TWi nT rai ts » CControlWi nTrai ts> class CW1nTra1tsOR { publi c: static DWORD CetWndStyle(0W0RD dwStyle) { return dwStyle 丨 t一 dwStyle I TWinTraits::GetWndStyle(dwStyle); } static DWORD CetWndExStyle(DWORD dwExStyle) { return dwExStyle 丨 t—dwExStyle 丨 TWinTraits::GetWndExSty1e (dwExStyle); } Using CWi nTrai tsOR, CMai nWi nTrai ts can be redefined like so: WINDOWI NG II Leave CFrameWinTraits styles alone. // Add the WS_EX_CLIENTEDGE bit to the extended styles, typedef CWinTraitsOR<0, WS_EX^CLIENTEDCE, CFrameWinTraits> CMainWinTraits; The Window Procedure To handle window messages, every window needs a window procedure. This win­ dow procedure is set in the 1 pfnWndProc member of the WNDCLASSEX structure used during window registration. You may have noticed that in the expansion of OECLARE_WND_CLASS and DECLARE_WND_CLASS_EX, the name of the window pro­ cedure is S tartW i ndowProc. StartW " ndowProc is a static member function of CWi ndowlmpl Base. Its job is to establish the mapping between the CWi ndowlmpl- derived object’s HWND and the object’s t h is pointer. The goal is to handle calls made by Windows to a window procedure global function and map them to a member function of an object. The mapping between HWND and an object’s th is pointer is done by the S tartW i ndowProc when handling the first window message.1 After the new HWND is cached in the CWi ndowlmpl -derived object’s member data, the object’s real window procedure is substituted for the StartW i ndowProc, as shown here: template :: StartWinckmProc(HWNO hWnd UINT uMsg WPARAM wParaw, LPARAM 1 Param) { CWindoivlmpl8aseT< T8ase, TWinTraits >* pThi s (CWindowlmplBaseT*) ^Module. ExtractCreateWndDataO; ATLASSERT(pThis !■ NULLj?' pTh1s>>n_hltfnd * hWnd; pthis->m_thunk.Init(pThis->CetWindowProc(), pThis); WNDPROC pProc « CWNOPROC)ft(pThi s->M.thunk.thunk); toMOPROC pOldProc • (WNDPROC): :S«tWindowLon9(hWnd, CW^WNDPROC, (UMC)pProc) return pProc(hWnd, uMsg, wParam, 1Paran); } cation. TOs happens before CreateWi ndow [Ex] returns with the HWNO. 4 2 4 ATL INTERNALS The m_thunk member is the interesting part. The ATL team had several differ­ ent options they could have used to map the HWND associated with each incoming window message to the object’s th is pointer responsible for handling the message. They could have kept a global table that mapped HWNDs to th is pointers, but the lookup time would grow as the number of windows grew. They could have tucked the th i s pointer into the window data,2 but then the application /component de­ veloper might unwittingly overwrite the data when doing work with window data himself or herself. Plus, empirically, this lookup is not as fast as one might like when handling many messages per second. Instead, the ATL team used a technique based on a set of assembly (ASM) in­ structions grouped together into a th unk,avoiding any lookup at ail. The term thunk is overused in Windows, but in this case, the thunk is a group of ASM in­ structions that keeps track of a CWi ndowlmpl object’s th is pointer and acts like a function, specifically, a window procedure. Each CWi ndowlmpl object gets its own thunk; that is, each object has its own window procedure. How a thunk can seem like an object (remember, it holds another object’s th i s pointer) and a function at the same time gets to the core of why ATL uses ASM to implement thunks. C+ + does not treat a function as a first-class object For example, you cannot give a func­ tion member data nor can you create new instances of functions at runtime. How­ ever, most CPUs are perfectly happy to let you compose machine code on the fly and execute it That’s what a thunk is: machine instructions built on the fly, one per window. For example, imagine two windows of the same class created like so: class CMyWi ndow : public CWi ndowlmpl CMyWindow wndl; wndl.C 「eate(.•.); CMyWindow wnd2; wnd2.Create(...); Figure 9.2 shows the per-window-class and per-HWND data maintained by Windows, the thunks, and the CWi ndowlmpl objects that would result from this example code. The thunk’s job is to replace the HWND on the stack with the CWi ndowlmpl ob­ ject's th i s pointer before calling the CWi ndowlmpl static member function Win- dowProc to further process the message. The ASM instructions that replace the HWND with the object’s t h i s pointer are kept in a data structure called the _Wnd- ProcThunk: #if defined(«M^IX86) 参pragma pack(pushtl) 2A window may maintain extra information via the cbWndExtra member of the WNDCLASS [EX] struc­ ture or via the CetWi ndowLong/SetWi ndowLong family of functions. WINDOWING struct .WndProcThunk { DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd) OWORD ffuthis; // BYTE // jmp WndProc DWORD m«re1proc; // relative jmp }; #pragma pack(pop) #elif defined (_M__ALPHA) ...// Alpha code elided for clarity •else terror Only Alpha and X86 supported #endif This data structure is kept per CWi ndowlmpl-derived object and is initialized by StartWindowProc with the object’s th i s pointer and the address of the static member function used as the window procedure. The m_thunk member that wndl: P«r HW N D Data Figure 9.2. One thunk per CWi ndowlmpl object 4 2 6 ATL INTERNALS StartW i ndowProc initializes and uses as the thunking window procedure is an instance of the CWndProcThunk class: class CWndProcThunk { public: union { ^AtlCreateWndData cd; .WndProcThunk thunk; }; void Init(WNDPROC proc, void* pThis) { #if defined LX_IX86) thunk.m^nov ■ 0x042444C7; //C7 44 24 OC thunk.m^this « (OWORD)pThis; thunk.nujmp » 0xe9; thunk.m_relproc * (int)proc-((int)this+sizeof CWndProcThunk)); #elif defined (—M^ALPHA) ••• // Alpha code elided for clarity #end1f II write block from data cache and II flush from instruction cache FlushInstructionCache(CetCurrentProcessQ, Athunk sizeof(thunk)); } Once the thunk has been set up in StartW i ndowProc, each window message is routed from the CW*i ndowlmpl object’s thunk to a static member function of CWi ndowlmpl to a member function of the CWi ndowlmpl object itself, as shown in Figure 9.3. On each window message, the thunk removes the HWND as provided by Windows as the first argument and replaces it with the CWi ndowlmpl-derived ob­ ject's th i s pointer. Once this first argument has been replaced, the thunk forwards the entire call stack to the actual window procedure. Unless the virtual CetWi n- dowProc function is overridden, the default window procedure is the Wi ndowProc static function shown here: template LRESULT CALLBACK CWindowlmplBaseT< TBase丨 TWinTraits >::W1ndowProc(HWNO hWnd UINT wParam JINT uMsg LPARAM wPi LPARAM IParam) { WINDOWING CWindowlnpl BaseT< TBase, TV/inTraits >* pThis a (CWindowInplBaseT< TBase, TWinTralts >*)hWnd; // set a ptr to this message and save the old value MSG msg * { pThis->«.hWnd, uMsg, wParaa, IParani, 0. { 0, 0 > >; const MSC* pOldMsg = pThi s->iLpCurrentMsg; pThis-xi^CurrentMsg * tesg; // pass to the message nap to process LRESULT IRes; BOOL bRet « pTh1s->ProcessWindowMessage(pThis->a.hWnd, uMsg, wParaa, IParaa, IRcs, 0); // restore saved value for the current message ATLASSERT (pThi s->irupCurrentMsg «» &msg); pThi s->m_pCurrentMsg « pOIdMsg; // do the default processing if message was not handled if(ibRet) { if(uMsg !» WM^NCOESTROY) IRes * pThi s-5 OefWi ndowProc(uMsg, wParam, 1Param); else t v Figure 9.3. Each message is routed through a thunk to map the HWND to the t h i s pointer 4 2 8 AT L INTERNALS // unsubclass, if needed tONC pfnWndProc - ::CetWindowLong(pThis->nuhWndl CWL.WNOPROC); IRes * pThis->0efWindowProc(uMsg, wParam, IParam); if(pThis->m_pfnSuperWindowProc !* ::OefWindowProc A& ::GetWindowLong(pThis->fiL.hWnd, CWL_WNDPROC) *» pfnWndProc) ::SetWi ndowLongCpThi s->m_hWnd, GWL_WNOPROC • } return 1Res; > The first thing Wi ndowProc does is extract the object’s t h is pointer out of the call stack by casting the HWND parameter. Since this HWND parameter has been cached in the objects m_hWnd member, no information is lost. However, if you over­ ride CetWi ndowProc and provide a custom WndProc for use by the window thunk, remember that the HWND is a t h i s pointer, not an HWND. After obtaining the objects th i s pointer, Wi ndowProc caches the current mes­ sage into the m一 pCurrentMsg member of the CWi ndowlmpl-derived object. This message is then passed along to the CWi ndowlmpl -derived object’s virtual member function ProcessWi ndowMessage, which n\ust be provided by the deriving class with the following signature: virtual BOOL ProcessWindo««Messa9e 〔HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM IParam, LRESULTA 1Result, OWOftO dv^sgMapIO); This is where any message handling is to be done by the object. For example, our CMai nWi ndow could handle window messages like so: (LONG)pThi s->m_pfnSuperWi ndowProc); // clear out wi ndow handle HWND hWnd » pThis->m_hWnd; pThis->nuhWnd » NULL; class CMainWindow : public CWi ndowlmpl { public: Wl NDOWING virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM 1Param, LRESULT& 1 Result, DWORD /*dwMsgMapID*/) { BOOL bHandled = TRUE; switchC uMsg ) { case WM_PAINT: 1 Result - OnPaintO ; break; case WM_DESTROY: 1 Result = OnDestroyO ; break; default: bHandled = FALSE; break; > return bHandled; private: LRESULT OnPaintO { PAINTSTRUCT ps ; HOC hdc = BeginPaintC&ps); RECT rect; GetClientRect(Arect); DrawText(hdc, —TCHello, Windows”), -1, Arect, DT_CENTER 丨 DT_VCENTER 丨 DT_SINGLELINE); EndPaint(&ps); return 0; } LRESULT OnDestroyO { PostQuitMessage(O); return 0; Notice how the message handlers are now member functions instead of global func­ tions. This makes programming a bit more convenient. For example, inside the OnPaint handler, Begi nPaint, GetCl ientRect, and EndPaint all resolve to member functions of the CMai nWi ndow object to which the message has been sent Also notice that returning FALSE from ProcessWi ndowMessage is all that is re­ quired if the window message is not handled. Wi ndowProc will handle calling Def- Wi ndowProc for unhandled messages. As a further convenience, OnFi nal Message is called by Wi ndowProc after the last message has been handled by the window and after the HWND has been zeroed out. This is handy for shutdown when used on the application’s main window. For ATL INTERNALS example, we can remove WM_DESTROY from our switch statement and replace the OnDestroy handler with OnFi nalMessage like so: virtual void CMainWindow::OnFinalMessage(HWND /*hwnd*/) { PostQuitMessage(O); } As you might imagine,writing the ProcessWi ndowMessage function is going to be a lot of boilerplate coding, tediously mapping window messages to function names. Soon, I'll show you the message map data structure that will handle this chore for us. Windows Superclassing The Windows ot)ject model of declaring a window class and creating instances of that class is similar to that of the C t ♦ object model. The WNDCLASSEX structure is to an HWND as a C + + class declaration is to a t h is pointer. Extending this analogy, Windows superclassing3 is like C + 十 inheritance. Superclassing is a technique in which the WNDCLASSEX structure for an existing window class is duplicated and given its own name and its own WndProc. When a message is received for that win­ dow, it’s routed to the new WndProc. Tf that WndProc decides not to handle that message fully, then, instead of being routed to DefWi ndowProc, the message is routed to the original WndProc. If you think of the original WndProc as a vir­ tual function, the superclassing window overrides the WndProc and decides on a message-by-messago basis whether to lot the base class handle the message or not. The reason for using superclassing is the same reason as for using inheritance of implementation, that is, the base class has some functionality that the deriving class would like to extend. ATL supports superclassing via the DECLARE_WND_ SUPERCLASS macro: #define OECLARE^WW^SUPERCUSSCWndCIassName, OrigWndClassName) \ static CWndClassInfoA CetWndClassInfoC) { \ static CWndClasslnfo wc * { { sizeof(WNDCLASSEX), 0, StartWi ndowProc, \ 0, 0, NULL, NULL, NULL, NULL. NULL, WndClassName, NULL \ OrigWndClassName, NULL, NULL, TRUE, 0, _T (”") \ return wc; \ 3The theory of Windows superclassing Is really beyond the scope of this book. For a more ii^depth discussion, see Win32 l^rvgramming, by Brent Rector and Joe Newcomer (Addison-Wesley, 1997). WINDOWI NG 431 The WndClassName is the naine of the deriving class’s window class. As with DECLARE一 WND—CLASS [一 EX], to have ATL generate a name, use NULL for this pa­ rameter. The OrigWndClassName parameter is the name of the existing window class from which you’d like to “inherit. For example, the existing edit control uses the ES_NUMBER style to indicate that it should only allow the input of numbers. If you wanted to provide similar func­ tionality but only allow the input of letters, you would have two choices. You could build your own edit control from scratch or you could superclass the existing edit control and handle WM—CHAR messages like so: // Letters-only edit control class CLetterBox : public CWi ndowlmpl { public: DECLARE_WND_SUPERCLASS(0, ••EDIT") virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM IParam, LRESULT& 1 Result, DWORD /*dwMsgMapID*/) { BOOL bHandled = TRUE; switch( uMsg ) { case WM—CHAR: 1 Result = OnChar((TCHAR)wPara_, bHandled); break; default: bHandled = FALSE; break; } return bHandled; private: LRESULT OnCharCTCHAR c, BOOL& bHandled) { ifC isalpha(c) ) bHandled = FALSE; else MessageBeepCOxFFFFFFFF); return 0; When an instance of CLetterBox is created, it will look and act just like a built-in Windows edit control, except that it will only accept letters and will beep otherwise. While powerful, it turns out that superclassing is rarely used. A much more commonly used technique is known as subclassing, which I’ll discuss later when I present CContai nedWi ndow. 4 3 2 ATL INTERNALS Handling Messages Whether it’s superclassing, subclassing, or neither, a mgjor part of registering a win­ dow class is providing the WndProc. The WndProc determines the behavior of the window by handling the 叩 propriate messages. You’ve seen how the default Wi n- dowProc provided by ATL routes the messages to the ProcessWi ndowMessage function provided by your CWi ndowlmpl-derived class. You’ve also seen how te­ dious it is to route messages from the ProcessWi ndowMessage function to the in- dividua] message handler member functions. Toward that end, ATL provides a set of macros for building a message map that will generate an implementation of ProcessWi ndowMessage for you. Providing the skeleton of the message map arc the BEGIN_MSG_MAP and END_MSG_MAP macros, defined as follows: #define B€CIN-MSC-KAP(theC1ass) \ public: \ BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, \ LPARAM 1Param, LRESULT& 1Result, DWORD dwMsgMapID * 0) \ { \ BOOL bHandled * TRUE; \ hWnd; uMsg; wParain; 1 Param; 1 Result; bHandled; \ swi tch(dwMsgMapID) { \ case 0: #define EN0J1S If you’d like to use a single message handler for a range of window messages, you can use the MESSACE_RANGE_HANDLER macro: #dcfinc MESSAGEJUNCEJtANDLERCmsgFi rst, nsgUst, func) \ if(uMsg >- msgFirst && uMsg <» msgLast) { \ bHandled - TRUE; \ IResult ■ func(uMsg, wParam, IParam, bHandled); \ if(bHandled) return TRUE; \ Using the message map macros, we can replace the sample implementation of ProcessWi ndowMessage with the following message map: BECIN_MSC_MAP(CMainWindow) MESSACE_HANDLER(WM_PAINT f OnPaint) END_MSC_MAP() This will expand roughly to the following implementation of ProcessWi ndow­ Message: // BECIN.MSC J4AP(CMai nWi ndow) BOOL ProcessWindowMessage(HWND hWndf UINT uMsg, WPARAM wParam, LPARAM IParam, LRESULT* IResult, OWORD dwMsgMapID - 0) { 4 3 4 ATL INTERNALS BOOL bHandled * TRUE; switch (dwMsgMapID) { case 0: // MESSACE^HANOLER(WW^PAINT, OnPaint) if(uMsg »■ WM.PAINT) { bHandled * TRUE; 1 Result * OnPaint(uMsg, wParam, IParam, bHandled); if (bHandled) return TRUE; / / END^SClJIAPO break; default: ATLTRACE2(at1TraceWindowing, 0, _T("Invalid message map ID (%i)\n"), dwMsgMapID); ATLASSERT(FALSE); break; } return FALSE; } There are two things of note here. First, if there is an entry in the message maf>} that message is assumed to be handled; that is, the default window procedure will not be called for that message. However, the B00L& bHandl ed is provided to each message handler, so you can change it to FALSE in the message handler if you’d like ATL to keep looking for a handler for this message. In the event that you are sub­ classing or superclassing, the original window procedure will receive the message only when bHandl ed is set to FALSE. Also, it’s possible that another message han­ dler further down the m叩 will receive the message. This is useful for message map chaining, which I’ll discuss soon. Ultimately, if nobody is interested in the message, DefWi ndowProc will get it The second interesting thing to note in this generated code is the member func­ tion signature required to handle a message map entry. All general messages are passed to a message handler with the following signature: LRESULT MessagcHaiuil«p(UINT nMsg, WPARAM wparam, LPARAM Iparam 600L& bHandled); WINDOWING You can either add the entry and the member function by hand, or, in the ClassView, you can right-click on any class with a message map and choose Add Windows Mes­ sage Handler (as described in Chapter 1). Unfortunately, however you add the handler, you’re still responsible for cracking your own messages. For example, if you’re interested in the optional HDC that may accompany the WM一 PAINT message, you’re responsible for performing the correct cast on the WPARAM. For example: LRESULT OnPaint(UINT nMsg, WPARAM wparam, LPARAM 1param, BOOL& bHandled) { HOC hdc = CHOC)wparam; • • • return 0; } Since at. last count there were over 300 standard messages, each with their own in­ terpretation of WPARAM and LPARAM, this can be quite a job. Hopefully, a future ver­ sion of ATL will provide this functionality. WM_COMMAND and WM—NOTIFY Messages Of the hundreds of Windows messages, ATL does provide a bit of message cracking assistance for two of them, WM^COMMAND and WM_N0TIFY. A Windows control uses these messages to communicate with its parent. I should point out that Windows controls are not OLE or ActiveX controls. A standard Windows control is 汪 child window whose class is defined by the Windows operating system. Some of these controls have been with us since Windows 1.0. Classic examples of Windows con­ trols include buttons, scrollbars, edit boxes, list boxes, and combo boxes. With the new Windows shell introduced with Windows 95. these controls were expanded to include things like toolbars, status bars, tree views, list views, and rich text edit boxes. Further, with the integration of Internet Explorer with the shell, more Win­ dows controls were introduced to include things like rebars, the date picker, and the IP address control. Creating a Windows control is a matter of using CreateWi ndow with the proper window class name, for example, EDIT, just like creating any other kind of window. Communicating with a child Windows control is a matter of calling SendMessage with the appropriate parameters. Communicating the other way, that is, between a child Windows control and a parent, is also done via Windows messages, specifi­ cally WM_C0MMAND or WM_N0TIFY. These messages provide enough information packed into WPARAM and LPARAM to describe the event of which the control is noti­ fying the parent, as shown here: 4 3 6 ATL INTERNALS wNotifyCode wID hwndCtl HIWORO(wParam); LOWORD(wParam); (HWND)IParam; // notification code // item, control, or accelerator // identifier II handle of control WM^NOTIFY idCtrl = (int)wParam; // control identifier pnmh = (LPNMHDR)IParam; // address of NMHDR structure Notice that the WM_NOTIFY message is accompanied by a pointer to an NMHDR, which is defined like this: typedef struct tagNMHOR { HWND hwndFrom; // handle of the control UINT idFrom; // control identifier UINT code; II notification code } NMHDR : For example, an edit box will notify the parent of a change in the text with a WM_COMMAND message using the EN_CHANCE notification code. The parent may or may not wish to handle this particular message, but if it did, it would like to avoid the responsibility of breaking out the individual parts of the command notification. ATL provides several macros for splitting out the parts of WM_COMMAND and WM_ NOTIFY messages. All these macros assume the following handler function signa­ tures (one for command messages and one for notify messages): LRESULT CoMandHandler(WORD wNotifyCode, WORD wID, HWNO hWndCtl, BOOL& bHandled); LRESULT NotifyHandlcrCint idCtrl, LPNMHOR pnmh, 600L& bHandled); The most basic handler macros are COMMAND_HANDLER and NOTIFY_HANDLER: LER(id, code, func) \ RDCwParam) SA \ (wParam)) \ WINDOWING bHandled - TRUE; \ IResult « func(HIWORD(wParam), LOWORD(wParam), (HWND)IParam bHandled); \ if(bHandled) return TRUE; \ > #define NOTIFY^MANOLERCid, code, func) \ 1f(uHsg ** WM^NOTIFY && \ id — ((LPNMHOR)1Param)->1dFrom SA \ code •• ((LPNMHOR)1Param)->code) \ { \ bHandled - TRUE; \ IResult ■ func((int)wParam. (LPNMHOR)IParam, bHandled); \ if(bHandled) return TRUE; \ > These basic handler macros let you specify both the id of the control and the com­ mand /notification code that the control is sending. Using these macros, handling an EN一 CHANGE notification from an edit control would look like this: C0MMAND—HANDLER(IDC_EDIT1, EN_CHANCE, OnEditlChange) Likewise, handling a TBN_BECINDRAC notification from a toolbar control would look like this: N0TIFY_HANDLER(I0C_T00LBAR1, TBN.BECINORAC, OnToolbarlBeginDrag) As a farther example, let’s add a menu bar to the sample Windows application like so: APIENTRY _tWinMain(HINSTANCE HINSTANCE LPTSTR int hinst, /♦hinstPrev*/ pszCmdLine, nCmdShow) // Initialize the ATL module II Create the main window CMai nWi ndow wnd; 4 3 8 ATL INTERNALS // Load a _enu HMENU HMenu = LoadMenu C_Nodule.GetResourcelnstance C), MAKEINTRESOURCE(IDR_MENU)); // Use the value 0 for the style and the extended style // to get the window traits for this class. wnd.Create(Of CWindow:rrcDefault, —T("Windows Application"), 0, 0, (UINT)hHenu); if( !wnd ) return -1; …II The rest is the same Assuming standard FilelExit. and Help I About items in our menu, handling these menu item selections would look like this: class CMai nWi ndow : public CWi ndowlmpl { public: BEGIN 一 MSG-MAP(CMai nWindow) MESSAGE—HANDLER(WM—PAINT, OnPaint) COMMAND_HANDLER(ID.FILE_EXIT, 0, OnFileExit) COMMAND__DLER(ID_HELP_>BOUT, 0, OnHelpAbout) END—MSG_MAP() • • • LRESULT OnFileExit(WORD wNotifyCode, WORD wID, HWND hWndCtl, B00L& bHandled); LRESULT OnHelpAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled); You may notice that menus are a little different from most Windows controls. Instead of using the ID of a child window as the first parameter, like for an edit con­ trol, we use the ID of the menu item. The command code itself is unused. In general, it’s not uncommon for the ID or the code to be unimportant when routing a message to a handler. One example we’ve already seen: handling a menu item doesn’t require checking the code. Another example of not worrying about the code would be if we wanted to route all events for one control to a single handler. Since the code is pro­ vided as an argument to the handler, further decisions could be made about how to handle a specific code for a control. To route events without regard for the specific code, ATL provides COMMAND_ID_HANDLER and NOTIFY_ID_HANDLER: WINDOWING #define COHMAND«IO„HANOLER(i d, func) \ if(uMsg " WM^COMMANO && id — LOWORD(wParam)) \ { \ bHandled * TRUE; \ IResult » func(HIWORD(wParam), LOWORO(wParam), (HVWO)IParam, \ ^ bHandled); \ if(bHandled) \ return TRUE; \ > •define NOTIFY_IOJidFrow) \ {、 bHandled * TRUE; \ IResult » func((int)wParamf (LPNMHDR)IParam, bHandled); \ If(bHandled) \ return TRUE; \ } 忽.•溢•賴 鑛 5奴 縫 終 •-谈 条 o 說 感 -成您?黑微叙 Using COMMAND«ID_HANDLER, our menu routing would more conventionally be written this way: COMMAND.JDJUNOLERCID.FILE^EXrr, 0nF1leEx1t) COPWAND^D_HANDLERCID^ELP_ABOUT, OnHel pAbout) Further, if you’d like to route notifications for a range of controls, ATL provides COMMAND.RANCE.HANDLER and NOTIFY_RANGE一HANDLER: #define COMNAIW.RANCEJ1AN0LER(i dFi rst, idUst, func) \ ifCuMsg « VW_C0MMAN0 && LOWORD(wParam) >• idFirst && LOWORD(wParam)\ <* idLast) \ { \ bHandled • TRUE; \ IResult « func(MIWORO(wParam), LOWORO(wParam), (HWND)IParam, \ bHandled); \ 彳f(bHandled) \ return TRUE; \ > • 440 ATL INTERNALS Ifdefine NOTIFY.RANCE.HANDLER(idFirst. idLast, func) \ if(uMsg « WM_NOTIFY SA ((IPNMHOR) 1 Param)->idFrom >*= idFirst M \ (CLPNMHDR)lParam)->idFrom <= idLast) \ {'| bHandled * TRUE; \ IResult ■ func((int)wParam, (LPNMHOR)IParam, bHandled); \ if(bHandled) \ 1 return TRUE; \ It’s also possible that you want to route messages without regard for their ID. This is useful if you’d like to use 狂 single handler for multiple controls. ATL supports this usage with C0MMAND_C0DE一HANDLER and NOTIFY_COOE_HANDLER: #define COMMANO_COOE_HANOLER(code, func) \ if(uMsg « WM一COMMAND SA code ** HIWORD(wParam)) \ { \ bHandled * TRUE; \ IResult ® func(HIWORD(wParam), LOWORD(wParam), (HWND)IParam. \ bHandled); \ if(bHandled) \ return TRUE; \ > #define NOTIFY.COOE.HANOLER(cd, func) \ if(uMsg ** WM—NOTIFY SA cd =* ((LPNMHOR)1Param)->code) \ { \ bHandled * TRUE; \ IResult * func((int)wParam, (LPNMHOR)IParam, bHandled); \ if(bHandled) \ return TRUE; \ Again, since the ID of the control is available as a parameter to the handler, you can make further decisions based on which control is sending the notification code. Why WM.NOTIFY? As an aside, you may be wondering why we have both WM_C0MMAND and WM一NOTIFY. After all, WM—COMMAND alone sufficed for Windows 1.0 through Win­ dows 3.x. However, when the new shell team was building the new controls, they WINDOWING 441 really wanted to send along more information than just the ID of the control and the notification code. Unfortunately, all the bits of both WPARAM and LPARAM were already being used in WM_COMMAND. Therefore, they invented a new message一 WM_NOTIFY— so that they could send a pointer to a structure as the LPARAM, keep­ ing the ID of the control in the WPARAM. However, if you examine the definition of the NMHDR structure, you’ll notice that there is no more information than was available in WM_COMMAND! Actually, there is a difference. Depending on the type of control that is sending the message, the LPARAM could point to something else that has the same layout as an NMHDR but has extra information tacked onto the end. For example, if you receive a TBN_BEGIN_DRAG,the NMHDR pointer w ill actually point to an NMTOOLBAR structure: typedef struct tagNMTOOLBAR { NMHOR hdr; int iltero; TBBUTTQN tbButton; int cchText; tPTSTR pszText; } NKTOOLBARt FAR* IPNMT00L8AR; Since the first member of the NMTOOLBAR structure is an NMDHR, it’s safe to cast the LPARAM to an NMHDR, even though it actually points at an NMTOOLBAR. If you like, you can consider this “inheritance” for C programmers. Message Chaining If you find yourself handling messages again and again in the same way, you may wish to reuse the message handler implementations. If you’re willing to populate the message map entries yourself, there's no reason you can’t use normal C+ + im­ plementation techniques: template class CFileHandler { public: LRESULT OnFileNewCWORD, WORD, HWND, BOOL&); LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&); LRESULT OnFileSave(WORD, WORD, HWND, BOOL&); LRESULT OnFileSaveAs(WORD, WORD, HWND, BOOL&); LRESULT OnFileExit(WORD, WORD, HWND, BOOL&); ATL INTERNALS class CMainWindow : public CWi ndowlmpl, public CFileHandlep { public: BEGIN_WSG_MAP(CMainWindow) MESSACE_HANDLER(WM_PAINT, OnPaint) // Route Messages to base class COMKANO_ID_HANDLER(ID_FILE_NEW, OnFileNew) CXJMMANDJEDJiANDLERCIO^FILE^OPEN, OnFileOpen) COMMANO-ID_HANDLER (ID.FILE^SAVE, OnFlleSave) COMNAND_IO_HANOLER(ID_FILEWSAVEJ^S, OnFileSaveAs) COMMANO^IDJIANDLER(ID^ILE.EXIT. OnFileExit) COMMANOJCD_HANDLER(IO-HELPJVBOUT, OnHelpAbout) ENO_MSCLMAP() This technique is somewhat cumbersome, however. If the base class were to gain a new message handler, for example, O nFil eCl ose, each deriving class would have to manually add an entry to its message map. What we’d really like is the ability to “inherit” a base class’s message map as well as a base class’s functionality. For this, ATL provides message chaining. Simple Message Chaining Message chaining is the ability to extend a class’s message map by including the message map of a base class or another object altogether. The simplest macro of the message chaining family is CHAIN一MSG—MAP: #def1nc CHAIN >(theChainClass) { \ if (theChaln ^ocessWindowMessage(hWnd, uMsg, wParamf 1Param,\ return TRUE; \. This macro allows chaining to the message map of a base class. For example: template class CFileHandler { public: // Mp 1n !>«$• class IResult)) \ WINDOWING 4 4 3 COMMAND. : COMMAND」 COMMAND一] BEGIN_MSCLMAP(CFileHandler) CO«MANO,ID_HANDLERCID_FILE_NEW, OnFileNew) .ID_HANDLER(ID-FILE_OPEN, OnFileOpen) _HANDLER(ID_FILE3AVE, OnFileSave) .ID_HANOLER(ID_FILE_SAVEJVS. OnFi1eSaveAs) COMMANDw EO_HANOtER (IO.FILE.EXIT • OnFHeExIt) END^MSCLHAPO LRESULT OnFileNew(WORD, WORD, HWND, BOOL&); LRESULT OnFileOpen(WORD, WORD, HWND, BOOL& ) ; LRESULT OnFileSave(WORD, WORD, HWND, BOOL&); LRESULT OnFi!eSaveAs(WORD, WORD, HWND, BOOL&); LRESULT OnFileExit(WORD, WORD, HWND, BOOL&); class CMai nWi ndow : public CWi ndowlmpl, public CFileHandler { public: BECIN_MSC_MAP(CMainWindow) MESSAGE__HANDLER(WM_PAINT, OnPaint) COMMAND^ID^HANDLER(ID_HELP^ABOUT, OnHelpAbout) // Chain to a base class 0) END_MSC_MAP() Any base class that provides its own implementation of ProcessWi ndowMessage (for example, with the message map macros) can be used as a chainee. Also notice that CFi 1 eHandl er is parameterized by the name of the deriving class. This is use­ ful when used with a static cast to obtain a pointer to the more derived class. For example, when implementing OnFi 1 eExit, you’ll need to destroy the window rep­ resented by the deriving class: template LRESULT CFi1eHandler::OnFi1eExit(WORD, WORD, HWND, BOOL&) { stat"ic_cast(th1s)->OestroyWindowO; return 0; 4 4 4 ATL INTERNALS Message chaining to a base class can be extended for any number of base classes. For example, if you wanted to handie the File, Edit, and Help menus in sep­ arate base classes, you would have several chain entries: class CMainWindow : public CWi ndowlmpl, public CFneHaiKller, public CEdi tHandlcr, public OielpHandler { public: BEGIN_MSC_MAP(CMainWindow) MESSAC 匕 HANDLER(WM一 PAINT, OnPaint) COMMAND_IO_HANDLER(ID_HELP^ABOUT, OnHelpAbout) // Chain to a base class CHAINJ1S(LmP(CFi 1 eHandl ep) CMSG_MAP(CEd1 tHandl er) kP(CHe1pHand1er) END_MSC. If, instead of chaining to a base class message map, you’d like to chain to the message map of a data member, you can use the CHAIN_MSC_MAP_MEMBER macro: ♦define 0Hndow> class CFileHandler { public: BECIN_MSC_MAP(CFi1eHandler) COMMAND_ID_HANDLER(ID_FILE—NEW, OnFileNew) COMMAND_ID—HANDLER(ID__FILE_OPEN, OnFileOpen) WINDOWING COMMAND_ID_HANDLER(ID_FILE_SAVEf OnFileSave) COMMAND_IO_HANDLER(ID_FILE_SAVE^AS, OnFileSaveAs) COMMAND_ID_HANDLER(ID^FILE_EXIT, OnFileExit) ENDJ^SC_MAP() CF11 eHandl er (TWi ndow* pwnd) : n__pwnd(pwnd) {} LRESULT OnFileNew(WORD, WORD, HWND, BOOL&); LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&); LRESULT OnFlleSave(WORD, WORD, HWND, BOOL&); LRESULT OnFileSaveAs(WORD, WORD, HWND, BOOL& ) ; LRESULT OnFileExit(WORD, WORD, HWND, BOOL&); private: TWindow* n_pwnd; An updated implementation would use the cached pointer to access the window instead of a static cast. For example: template LRESULT CFi1eHandler::OnFi1eExit(WORD, WORD wID, HWND, BOOL&){ ■_pwnd->DestroyWindowO; return 0; Once we have an updated handler class, using it looks like this: class CMainWindow : public CWi ndowlmpl { public: BEGIN_MSG—MAP(CMai nWindow) MESSAGE_HANDLER(WM_PAINT, OnPaint) COMMAND_ID_HANDLER(ID_HELP^ABOUT, OnHelpAbout) // Chain to the CFileHandler nenber CHAIN_MSCLHAP_MEMBER(««handlerFI1e) END_HSC_MAP() • • • CMainWindowO : m_handler File (this) {> private: CF11 eHandler n.handlerFile; 446 ATL INTERNALS Alternate Message Maps It’s possible to break a message map into multiple pieces. Each piece is called an alternate message map. Recall that the message map macros expand into a switch statement that switches on the dwMsgMapID parameter to ProcessWi ndowMes­ sage. The main part of the message map is the first part and is identified with a 0 message map ID. An alternate part of the message map, on the other hand, is distin­ guished with a nonzero message map ID. As each message eomes in ,it’s routed first by message map ID and then by message. When a window receives its own messages and those messages chained from another window, an alternate part of the message map allows the window to distinguish where the messages are coming from. A mes­ sage map is broken up into multiple parts using the ALT一 MSCMAP macro: . . . . . _ _ . _ _ _ _______I fdefine ALTJ«CJ { public: BECIN^MSCJIAP(CView) // Handle CView messages MESSACE^HANDLER(WM.PAINT, OnPaint) // Handle messages chained from the parent wi ndow ALT_MSCLMAPC1) COMMANO_HANDLER(ID—EDIT_C0PY) END_MSG_MAP() Since the message map has been split, the child window (CVi ew) will only receive its own messages in the main part of the message map. However, if the main win­ dow were to chain messages using CHAIN_MSG_MAP_MEMBER> the child would re­ ceive the messages in the main part of the message map, not the alternate part To chain messages to an alternate part of the message map, ATL provides two macros, CHAINJiSC^IAP^ALT and CHAINJ1SC^MAP_ALT_MEMBER: WINDOWING 447 For example, for the parent window to route unhandled messages to the child, it can use CHAIN_MSG_ALT^MEMBER like so: class CMai nWi ndow : … { public: BECIN.MSG_MAP(CMainWindow) MESSAGE_HANDLER(WM_CREATEf OnCreate) • • • // Route unhandled Messages to the child window CHAIN_MSCJUPjU_T_MEHBER(iL«view, 1) END_MSCl.MAPO LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&) { return m_vi ew.Create(m_hWnd ? CWi ndow::rcDefault) ? 0 : -1; } 參•參 private: CView m_vi ew; Dynamic Chaining Message map chaining to a base class or a member variable is useful, but not as flexible as we might like. What if you’d like a looser coupling between the window that sent the message and the handler of the message? For example, the MFC WM.COMMAND message routing depends on just such loose coupling. The view re­ ceives all the WMCOMMAND messages initially, but the document handles file-related ATL INTERNALS command messages. If we’d like to construct such a relationship using ATL, we have one more chaining message map macro, CHAIN_MSG_MAP一 DYNAMIC: #define CHAINJiS ; 0 ; CMessageMap is actually a fairly poorly named class. A better name would be something like CMessageHandler or even CMessageProcessor. All that CMes- sageMap does is guarantee that every class that derives from it will implement ProcessWi ndowMessage. In fact, CWindowlmpI derives from it as well, making an implementation of ProcessWi ndowMessage mandatory for CWi ndowlmpl- derived classes. The reason that a secondary message processor must derive from CMessageMap is so that it can be placed m the ATL_CHAIN_ENTRY structure man­ aged by the primary message processor struct ATl^CHAIfLENTRY { DWORD m^dwChainlO; CMessageMap* irupObject; OWORD nudwMsgMapID; Second, a primary message processor must derive from CDynami cChai n, a base class that manages a dynamic array of ATL一 CHAIN一 ENTRY structures. WINDOWING 449 CDynami cChai n provides two important member functions. The first, SetChai n- E ntry, is used to add an ATICHAIN_ENTRY structure to the list: BOOL COynamicChain::S«tCha1nEntry(DWORD dwChainlO, CMessageMap* pObject, DWORD dMMsgflapID * 0); I The other important function, C all Chain, is used by the CHAIN_MSC_MAP. \ DYNAMIC macro to chain messages to any interested parties: 800L OynamicChain::Ca11Cha1n(0W0R0 dwChainIO» HM40 hWnd, UINT uMsg WPARAM MParam, LPARAM IParam, LRESULTA IResult); As an example of this technique, imagine an application built using a simplified document/view architecture. The main window acts as the frame, holding the menu bar and two other objects, a document and a view. The view manages the client area of the main window and handles the painting of the data maintained by the docu- ment. The view is also responsible for handling view-related menu commands, such as EditlCopy. The document is responsible for maintaining the current state as well as handling document-related menu commands, such as FllelSave. To route com­ mand messages to the view, the main window will use altemate-message-map, member function chaining (which weVe already seen). However, to continue rout­ ing commands from the view to the document, the main window, after creating both the document and the view, will “hook” them together, if you will, using SetChai n- Entry. Any messages unhandled by the view will automatically be routed to the document by the CHAIN_MSG__MAPJ)YNAMIC entry in the view’s message map. The document, finally, will handle any messages it likes, leaving unhandled messages for DefWi ndowProc. The main window creates the document and view and hooks them together class CMai nWi ndow : public Orfi ndowlmpl { public: BECIN_MSC_MAP(CMainWindow) // Handle main wi ndow messages MESSACE_HANDLER(WM_CREATE, OnCreate) • •參 // Route unhandlttd Mssages to the CHAIHJfSGJIAPJ^LTJ®WER(^_v1 a w , 1) 450 AT L INTERNALS // Pick up messages the view hasn’t handled COMMAND_ID_HANDLER(ID_HELP_ABOUT, OnHelpAbout) END—MSG 一 MAPO CMai nWi ndow() : m—doc(this), m_view(&m_doc) { // 「 k up the document to receive messages from the view m__v .SetChainEntryCl, &m_doc, 1); > private: H Create the view to handle the main window's client area LRESULT OnCreate(UINT, WPARAM, LPARAM, B00L&) { return (m_vi ew.Create(m_hWnd, CWi ndow::rcDefault) ? 0 : -1); } LRESULT OnHelpAbout(WORD, WORD, HWND, BOOL&); vi rtual voi d OnFinalMessage(HWND /*hwnd*/); private: CDocument m_doc; CVi ew n_vi ew ; The view handles the messages it wants and chains the rest to the document: template class CView : public CWi ndowlmpl, // Derive from CDynamicChain to support dynamic chaining public CDynamicChain { public: CView(CDocument* pdoc) : m_pdoc(pdoc) { // Set the document-managed string m_pdoc->SetString(—T("ATL Doc/View")); } # BECIN_MSC_MAP(CView) // Handle view messages MESSAGE_HANDLER(WM_PAINT, OnPaint) ALT一MSC_MAP(1) // Handle messages from the main window CHAINJ1SG_MAP_DYNAMIC(1) // Route messages to the document END 一 MSG—MA P Q WINDOWING private: LRESULT OnPaintCUINT, WPARAM, LPARAM, BOOL&); private: // View caches its own document Q)ocument* m_pdoc; The document handles any messages it receives from the view: template ctypename TMainWindow> class COocument •• // Derive from CMessageMap to receive dynamically chained messages public CMessageMap { public: BEGIN J^SG_MAP(CDocument) // Handle messages from the view and the main frame ALT一MSG_MAP(1) COMMAND—ID—HANDLER(ID_FILE_NEW, OnFi1eNew) COMMAND_ID_HANDLER(ID_FILE—OPEN, OnFi1eOpen) COMHAND_ID_HANDLER(ID^FILE_SAVE, OnFi1eSave) COMMAND—ID_HANDLER(ID_FILE_SAVE_AS• OnFileSaveAs) COMHAND_ID_HANDLER(ID_FILE^EXIT, OnFileExit) END_MSG_MAP() CDocument(TMai nWi ndow* pwnd) : m一 pwnd(pwnd) { *m_sz = 0: } void SetString(LPCTSTR psz); LPCTSTR CetStringC); // Message handlers private: LRESULT OnFileNew(WORD, WORD, LRESULT OnFil eOpen (WORD, V.ORD, LRESULT OnFileSave(WORD, WORD, LRESULT OnFileSaveAs(WORD, WORD, HWND, BOOL&) LRESULT OnFileExit(WORD, WORD, HWND, BOOL&); HWND, B00L& ) ; HWND, BOOL&) HWND, BOOL&) private: TMainWindow* m_pwnd; TCHAR m_sz[64]; 452 A1 L INTERNALS Filtering Chained Messages In many ways, ATLs CMessageMap is bko MFC's CCmdTarget. However, MFC makes a distinction between romnianri nu'ssagos and noncommand messages. While its useful for the and thr fioramerit to participate in handling the main window's command messages, th(、rosl ( for exai^iple, WM_XXX) aren't nearly so use­ ful. The view and the docuinent manage to ignore the rest of rho messages using al­ ternate parts of their mossago maps, bui si ill, il would be nice if every message vveren'l routed this way. rnfortunately, lh(*res no built-in way to route only conv inand messages using the message chaining macros. However, a custom macro ro»jld do the trick: #define CHAIN_COMMANO_DYNAMIC(dynaChainIO) { \ if ((uVlsg == WM_C0M^AN0) && \ CDynami cChai n::CalIChai n(dynaChai nIO, hWnd, uMsg, wParam, 1 Pa ram,\ IResult)) \ return 了RUE; \ This mac ro would chain only WM—COMMAND messages very much like MFC does. I iowevor, you'd st ill need corresponding equivalents for the nondynamic message chaining macros. CDialoglmpl Dialogs represent, if you will, a dotiararivc style of usor inierfaco (levolopmont. While normal windows provide all kinds of flexibility (you can put anything you want, in the client area of a window), dialogs are rnor<、static. Actually, dialogs are just windows whose layout has been predetermined. The built-in dialog box win­ dow class knows how to interpU dialog box resources to create and manage the child windows that make up a dialog box. To show a dialog box m odally— that is, while the dialog is visible ihe parent is inaccessible一Windows provides the Dia- logBoxParam4 function: int Di aiogBoxParam( HINSTANCE hlnstance, // LPCTSTR IpTemplate, // HWND hWndParent, // handle to application instance identifies dialog box template handle to owner window lThe OialogBox function is merely a wrapper around DialogBoxParam, passing 0 for the dwlni t- Param argumont. WINDOWING handle to application instance identifies dialog box template handle to owner window pointer to dialog box procedure initialization value HWND Cr«atc01alogParaa( HINSTANCE hlnstance, LPCTSTR IpTemplate, HWND hWndParent, OLCPROC IpDialogFunc, LPARAM dwlnitParam); The result of the Di a i ogBoxPa ram function is the command that closes the dialog, for example, IDOK or IDCANCEL. To show the dialog box modelessly— that is, the parent window is still accessible while the dialog is visible— the C reateD i a i og- Param5 function is used instead: Notice that the parameters to CreateDi ai ogParam are identical to Di al ogBox- Param, but the return value is different The return value from CreateDialog- Param represents the HWND of the new dialog box window, which will live until the dialog box window is destroyed. Regardless of how a dialog is shown, however, developing a dialog is the same. First, you lay out a dialog resource using your favorite resource editor. Second, you develop a dialog box procedure (DlgProc). The DlgProc will be called by the win­ dow procedure of the dialog box class to give you an opportunity to handle each message (although you never have to call DefWi ndowProc in a DlgProc). Third, you call either Di al ogBoxPa ram or CreateDi a l ogParam (or one of the variants) to show the dialog. This is the same kind of grunt work we had to do when we wanted to show a window, namely, register a window class, develop a window procedure, and create the window. And just as ATL lets us work with CWi ndow-derived objects instead of raw windows, ATL also lets us work with CDi aloglmpl^derived classes instead of raw dialogs. Showing a Dialog CDi a log Impl provides a set of wrapper functions around common dialog opera­ tions (like Di alogBoxParam and CreateDi a l ogParam): DLGPROC IpOialogFunc, // pointer to dialog box procedure LPARAM dwlnitParam); // initialization value template class ATI_N0^VTA8LE CDialoglmpI :public CDialoglmplBaseT { public: // // II // 6Iikewiae, CreateDi alog is • wrapper around the CreateDi al ogPara« Ainction. 454 ATL INTERNALS // modal dialogs int DoModal(HWND hWndParent - ::CetActiveWindow(), LPARAM dwInitParam =* NULL) { ^Module.AddCreateWndDataC&w_thunk•cd, (CDialoglmplBaseT*) this); return ::Di alogBoxParam(_Module.CetResourcelnstance(), BOOL EndOialogCint nRetCode) { return ::EndOialog(m_hWnd, nRetCode); > // modeless dialogs HWND Create(HWND HWndParent, LPARAM dwInitParam « NULL) { _Module.AddCreateWn(fi)ata(touthunk.cd, (CDialoglmplBaseT*) this); HWND hWnd ■ ::CreateOialogParam( ule.GetResourcelnstanceO t return HWnd; } BOOL OestroyWindowO { return : :OestroyWindow(frL.hWnd); } There are a couple of interesting things going on in this small class. First, notice the use of the thunk. ATL sets up a thunk between Windows and the ProcessWin- dowMessage member function of your CDi al oglmpl -based objects, just as it does for CWi ndowlmpl-based objects. In addition to all the tricks that Wi ndowProc per- MAKEINTRESOURCE(T::IOD), hWndParent, (DLCPROC)T::StartOialogProc dwInitParam); MAKEINTRESOURCE(T::100), hWndParent, (DLCPROC)T::Start0ia1ogProc dwInitParam); WINDOWING 445 COMMAND—ID 一 HANDLER(ID_FILE_SAVE, OnFileSave) COMMAND_ID_HANDLER(ID_FILE_SAVF_AS, OnFileSaveAs) COMMAND_ID_HANDLER(ID_FILE_EXITt OnFileExit) END_MSC_MAP() CFi1eHandler(TWi ndow* pwnd) : m_pwnd(pwnd) {} LRESULT OnFileNew(W0RD, WORD, HWND, BOOL&); LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&); LRESULT OnFileSave(WORD, WORD, HWND, BOOL&); LRESULT OnFileSaveAs(WORD, WORD, HWND, BOOL&); LRESULT OnFileExit(WORD, WORD, HWND, BOOL&); private: TWindow* m 一 pwnd; An updated implementation would use the cached pointer to access the window instead of a static cast. For example: template LRESULT CFi1eHandler::OnFileExit(WORD, WORD wID, HWND, BOOL&){ *_pwnd->DestroyWi ndowC) ; return 0; } Once we have an updated handler class, using it looks like this: class CMainWindow : public CWi ndowlmpl { public: BEGIN_MSC_MAP(CMainWindow) MESSAGE_HANDLER(WM_PAINT, OnPaint) COMMAND_ID_HANDLER(ID_HELP^ABOUT, OnHelpAbout) // Chain to the CFileHandler member CHAIN„MSC_HAP_MEMBER(«_hancn erFi1e) END^MSC_MAP() • • • CMainWindowO : m_handlerFile(this) {} private: CFi1eHandler n.hand1erFi1e ; 456 ATL INTERNALS symbol called IDD indicating the resource identifier.6 For example, assuming a re­ source ID of IDD_ABOUTBOX, you would implement a typical About box like so: class CAboutBox : public CDi aloglmpl { public: BECirCHSG_MAP(CAbo4JtBox) MESSAGE_HANOLERCVyM_INITDIALOC, OnlnitDialog) CONMAND_ID,HANDLER(IDOK, OnOK) END.MSC_MAPC) enu_ {100 = IOOJ^BOUTBOX >; private: LRESULT OnInitDialog(UINT, WPARAM, LPARAM, B00L&) { CenterWi nd o w O ; return 1; } LRESULT OnOK(WORD, UINT, HWND, B00L&) { EndDialog(IDOK); return 0: CAboutBox has all the elements. It derives from CDi a l o g lm p l, has a message map, and provides a value for IDD that indicates the resource ID. If you want to use the Dialog object type in the ATL Object Wizard,7 it will get you started, but it’s not difficult to do by hand. Using a CDi al oglmpl -derived class is a matter of creating an instance of the class and calling either DoModal or Create. For example: LRESULT CMainWindow::OnHelpAbout(WORD, WORD, HWND, B00L&) { CAboutBox dig; dig.DoModal(); return 0; > 6 Why the dialog resource ID isn't just a template parameter, I have no idea. 7 Assuming you've convinced the ATL Object Wizard to insert an object into yoiir project. See Faking out the ATL Object Wizard in Chapter 1 for some advice WINDOWING Simple Dialogs For simple modal dialogs, like About boxes, that don’t have any interaction re­ quirements beyond the standard buttons (such as OK and Cancel), ATL provides CSimpleDialog : template class CSiMpleOlalog : public CDialoglmplBase { public: in 1(HWND hWndParent * ::CetActiveWindow()) { AddCreateWndData(&m_thunk.cd, (CDialoglmplBase*)this); int nRet « ::OialogBox(_Module.CetResourceInstance(), m_hWnd *» NULL; return nRet; } . ' :爾 BECIH^HSC^AP (CSi mp 1 eOi al og) MESSACE_HANOL£RCVW-INITOIALOC, OnlnitOi alog) iE.HANDLER(IDOK, IONO , OnCloseCmd) LRESULT OnlnitOialog(UINT, WPARAM, LPARAM, BOOL&) { if (t_bCenter) CenterWindow(CetParent()); return TRliEi .藤, LRESULT OnCloseCmd(WORO, WORD wID, HWND, BOOL&) { ::EndDialog(m.hWnd• wID); return 0; Notice that the resource ID is passed as a template parameter, as is a flag indi­ cating whether the dialog should be centered. This reduces the definition of the CAboutBox class to the following type definition: typedef CSi mpleDi alog CAboutBox; MAKEINTRESOURCE(t^wOlgTemplatelO) hWndParent, (DLCPROC)StartOi alogProc); However, the usape of CAhnntBoy the ATL INTERNALS Data Exchange and Validation Unfortunately, most dialogs aren't simple. In fact, most are downright complicated. This complication is mostly due to two things: writing data to child controls man­ aged by the dialog and reading data from child controls managed by the dialog. Ex­ changing data with a modal dialog typically goes like this: 1. Application creates an instance of a CDi a l oglm pl -derived class. 2. Application copies some data into the dialog object’s data members. 3. Application calls DoModal. 4. Dialog handles WM一 INITDIALOG by copying data members into child controls. 5. Dialog handles the OK button by validating the data held by child controls. When the data is invalid, the dialog complains to the user and makes him or her keep trying until he or she either gets it right or gets frustrated and hits the Cancel button. 6. When the data is valid, the data is copied back into the dialog’s data members and the dialog ends. 7. When the application gets IDOK from DoModal,it copies the data from the dia­ log data members over its own copy. If the dialog is to be shown modelessly, the interaction between the application and the dialog is a little different, but the relationship between the dialog and its child controls is the same. A modeless dialog sequence goes like this (differences from the modal case are shown in italics): 1. Application creates an instance of a CDi a l oglm pl -derived class. 2. Application copies some data into the dialog object’s data members. 3. Applications calls Create. 4. Dialog handles WM_INITOIALOG by copying data members into child controls. 5. Dialog handles the A pply button by validating the data held by child controls. When the data is invalid, the dialog complains to the user and makes him or her keep trying until he or she either gets it right or gets frustrated and hits the Can­ cel button. 6. When the data is valid, the data is copied back into the dialog’s data members and the application is notified8 to read the updated data from the dialog. 7. When the application is notified,it copies the data from the dialog data mem­ bers over its own copy. A custom window message sont to the dialog’s parent is excellent for this duty. WINDOWING S e t S trm t) Figure 9.4. A dialog that needs to manage data exchange and validation Either way, modal or modeless, the dialog’s job is to exchange the data back and forth between its data members and the child controls and to validate it along the way. MFC has something called DDX/DDV (Dialog Data Exchange/Dialog Data Vali­ dation) for just this purpose. ATL has no such support, but it turns out to be pretty easy to build yourself. For example, to beef up our standalone Windows application sample, imagine a dialog that allowed one to modify the display string, as shown in Figure 9.4. The CDi a i oglm pl -based class would look like this: class CStringDIg : public CDialoglmpl { public: CStringDlgC) { *m_sz = 0; } BECIN_MSC_MAP(CStringDlg) MESSACE^HANDLERCWM.INITDIALOC, OnlnitDialog) COMMAND,ID_HANDLER(IDOK r OnOK) COMMAND—ID_HANDLER(IDCANCEL, OnCancel) END_MSC_MAP() enum { IDO - IDD—SET—STRINC }; TCHAR m_sz[64]; private: bool CheckValidStri ng() { // Check the length of the string int cchString = ::CetWindowTextLengthCCetDlgItem(IDC_STRINC)#); return cchString ? true : false; } LRESULT OnlnitDialog(UINT, WPARAM, LPARAM, B00L&) { CenterWindowO; 4eO A T I IN T E R N A L S II Copy the string from the data member to the child control (DOX) SetDl glteisText (IDC_STRING • m _sz); return 1; // Let dialog manager set initial focus } LRESULT OnOK(WORD, UINT, HWND, BOOL&) { // Cowplain if the string is of zero length (DOV) HfC '.CheckVaTidStringO ) { MessageBox('TIease enter a string", "Hey!"); return 0; > // Copy th€ string from the child control to the data member (DOX) GetDl glteaText (IDC.STRINC, n_sz, 1 engthof (■_«)); EndDialog(ICX)K); return 0; > LRESULT OnCancelCWORD, UINT, HWND, B00L&) { EndDialog(IDCANCEL); return 0; In this example, DDX-like functionality happens in Onlni tD ialog and OnOK. O nlni tDi alog copies the data from the data member into the child edit control. Likewise, OnOK copies the data from the child edit control back to the data member and ends the dialog, if the data is valid. Checking the validity of the data (DDV-like) is performed before the call to EndDi alog in OnOK by calling the helper function CheckVal i d S tr i ng. I decided that a zero-length string would be too boring, so 】 made it invalid. In that case, OnOK puts up a message box and doesn’t end the dia­ log. To be fair, MFC would have automated all this with a macro-based table, which makes handling a lot of DDX/DDV chores easier, but ATL certainly doesn’t prohibit data exchange or validation. In fact, I can do even better in the data validation area with this example. Al­ though this example, and MFC-based DDX/DDV, only validates when the user presses the OK button, sometimes it’s handy to validate as the user enters the data For example, by handling EN_CHANCE notifications from the edit control, I can check for a zero-length string as the user enters it If the string ever gets to zero, dis­ abling the OK button would make it impossible for the user to attemDt to commit WINDOWING the data at all,making the complaint dialog unnecessary. The following updated code shows this technique: class CStringDIg : public CDi aloglmpl { public: • • • BEGIN_MSC_MAP(CStringDlg) • • • COMMAND_HANDLER(IEXLSTRINC, EN_CHANCE, OnStringChange) END 一 MSG_MAP() private: void CheckValidString() { // Check the length of the string int cchString *= ::GetWindowTextLength(CetDlgItem(IDC_STRINC)); // Enable the OK button only if the string is of nonzero length ::EnableWindow(GetD1gItem(IDOK), cchString ? TRUE : FALSE); } LRESULT OnlnitOialog(UINT, WPARAM, LPARAM, BOOL&) { CenterWindow(); // Copy the string from the data member to the child control (DOX) SetDl gltemText (IDC_STRINC, rrusz); // Check the string length (DOV) CheckValidStringC); return 1; // Let dialog manager set initial focus > LRESULT OnStringChange(WORO, UINT, HWND, BOOL&) { // Check the string length each time it changes (DOV) CheckVali dStri ng(); return 0; … // The rest is the same In this case, notice that O nlni tDi alog lakes on some DDV responsibilities, while OnOK loses some. In O nlni tD i al og, if the string starts with a zero length, the OK 462 ATL INTERNALS button is immediately disabled. In the OnStringChange handler for EN一 CHANGE, as the text in the edit control changes, we revalidate the data, enabling or disabling the OK button as necessary. Finally, we know that if we reach the OnOK handler at all, the OK button must be enabled and the DDV chores must already have been done. Neither ATL nor MFC can help us with this kind of DDV, but then again, nei­ ther hinders us from providing a user interface that handles both DDX and DDV in a friendly way. Windows Control Wrappers Child Window Management You may have noticed in the last two examples that whenever I needed to manip­ ulate a child control, such as getting and setting the edit control’s text or enabling and disabling the OK button, I used a dialog item function. The ultimate base class of CDi a l o g lm p l, CWi ndow, provides a number of helper functions to manipulate child controls: class CWindow { public: • •癱 // Oialog-Box Item Functions BOOL CheckOIgButton(i nt nI08utton, UINT nCheck); BOOL CheckRadioButton(int nIDFirstButton, int nIDLastButton, int nIOCheckButton); int 0 1gOirList(LPTSTR IpPathSpec, int nIOList box, int nIOStaticPath, UINT nFileType); int 01gDirListCo«boBox(LPTSTR IpPathSpec, int nIDComboBox, int nIOStaticPath, UINT nFileType); BOOL 01gDirS«lect(LPTSTR IpString, int nCount, int nIDList box); BOOL DIgOirS«l€CtCo«boBox(LPTSTR IpString, int nCount, int nIDComboBox); UINT CetOIglt«Mlnt(int nIO, 800L* lpTrans « NULL, BOOL bSigned « TRUE) const; UINT CetOIglteaText(int nIO, LPTSTR IpStr, int nMaxCount) const; BOOL CetOIgltMTcxtCint nID, BSTR& bstrText) const; WINDOWING HWND GctNcxtDIgCroupItea(HWNO HWndCtl, BOOL bPrevious • FALSE) const; HWND CctNcxtDlgTablteaiCHWNO hWndCtl. BOOL bPrevious * FALSE) const; UINT IsDIgButtonCheckedCint nIDButton) const; LRESULT SendOlglteaMessagc(int nID, UINT message, WPARAM wParam ■ 0, LPARAM 1Param » 0); BOOL SetDlgltenlnt(int nID, UINT nValue, BOOL bSigned ■ TRUE); BOOL SetJJI gltemText (int nID, LPCTSTR IpszString); Although ATL adds no overhead to these functions, since they’re just inline wrappers of Windows functions, you can just feel that something’s not quite as efficient as it could be when you use one of these functions. Every time we pass in a child control ID, the window is probably doing a lookup to figure out the HWND and then calling the actual function on the window. For example, if I ran the zoo, Set­ Dl gltemText would be implemented as follows: BOOL SetDlgltemText(HWND hwndParent, int nID, LPCTSTR IpszString) { HWND hwndChild : GetDlgltemChwndParent, nID); if( !hwndChild ) return FALSE; return SetWindowText(hwndChiId, IpszString); While using that implementation is fine for family, when your friend comes over, it’s time to pull out the good dishes. I’d prefer to cache the HWND and use SetWi ndow- Text. Plus, if I don’t want to refer to a dialog or a main window with an HWND, why would I want to refer to my child windows with UINT? Instead, I find it convenient to wrap windows created by the dialog manager with CWi ndow objects in O nlni t- D ialo g: LRESULT CSt ri ngDlg::Onlni tDi alog(UINT, WPARAM, LPARAM, B00L&) { CenterWindow(); // Cache HWNDs i^.edi t.AttachCCetDlgltcaCIDC-STRINC)); M^ok.Attach(GetDlgltoi(IDOK)); 464 ATL INTERNALS II Copy the string from the data member to the chi Id control (DDX) ■_edit.SetWi ndowText Cm_sz); II Check the string length (DDV) CheckValidString(); return 1; // Let dialog manager set initial focus Now the functions that used any of the dialog item family of functions can use CWi ndow member functions instead: void CStri ngDlg::CheckVali dStri ng() { II Check the length of the string int cchString = m_edit.GetWindowTextLengthC); II Enable the OK button only if the string is of nonzero length iL.ok. Enabl eWindow(cchString ? TRUE : FALSE); } LRESULT CStringDIg::OnOK(WORD, UINT, HWND, BOOL&) { II Copy the string from the chi Id control to the data member (DDX) w 一 edi t.C e t W i n d o w T e x t ^engthof(m_sz)); EndDialog(IDOK); return 0; A Better Class of Wrappers Now, my examples have been purposefully simple. A dialog box with a single edit control is not much work, no matter how you build it. However, let's mix things up a little. What if we were to build a dialog with a single list box control instead, as shown in Figure 9.5? This simple change makes the implementation quite a bit more complicated. Instead of being able to use SetWi ndowText, as we could with an edit control, manipulating a list box control means using special Windows messages. For example, populating the list box and setting the initial selection means using the following code: LRESULT CStri ngL i stDlg::0nlnitDialog(UINT, WPARAM, LPARAM, BOOL&) { CenterWindow(); WINDOWING 465 Figure 9.5: A dialog with a list box // Cache list box HWND m_lb•Attach(CetDlgItem(IDC_LIST)); I I Populate the list box m j\b .SendMessage(LBJVDDSTRINC, 0, (LPARAH)—T("Hello, ATL")); a.1 b.SendMessageCLBJUX)STRING( 0, (LPARAM)一 T("A*fiTt ATL Cool?")); w L \b .SendMessa9e(LB_ADDSTRINCf 0, (LPARAM)一 T("ATL 1s your friend'*)) // Set initial selection 1nt n = m j\b .SendMessageCLB_FINDSTRlNG f ifC n == LB.ERR ) n = 0; O b .SendNessage(LB3ETCURSEL9 n); (LPARAM)n.sz) return 1; // Let dialog manager set initial focus Likewise, pulling out the final selection in OnOK is just as much fun: LRESULT CStringListDlg::0n0K(W0R0, UINT. HWND, B00L&) { // Copy the selected int n = Ob.S€ndMessageCLB_GETOJRSEL); 1f( n == LB.ERR ) n » 0; m j\b .S€ndM«ssage(LB.CETTEXT, n, (LPARAM)(LPCTSTR)«_S2> ; EndDialogCIDOK); return 0; > 466 ATL INTERNALS The problem is that, although CWi ndow provides countless wrapper functions common to all windows, it does not provide any wrappers for the built-in Windows controls, such as list boxes. And whereas MFC provides such wrapper classes (for example, CLi stBox), ATL doesn’t . . . officially. However, unofficially, buried deep in the a t l con9 sample lives an undocumented and unsupported set of classes that fit the bill nicely. The a tl controls .h file defines the following Windows control wrappers inside the ATLControl s namespace: CAnimateCtrl CDateTimePi ekerC trl CHeaderCtrl CListBox CProgressBarCtrl C S tatic CToolInfo CTreeViewCtrl CButton CDragListBox CHotKeyCtrl CListViewCtrl CReBarCtrl CStatusBarCtrl CToolTipCtrl CTreeViewCtrlEx CComboBox CEdit CImageList CMonthCalendarCtrl CRichEditCtrl CTabCtrl CTrackBarCtrl CUpDownCtrl CComboBoxEx CFlatScrollBar CIPAddressCtrl CPagerCtrl CScrollBar CToolBarCtrl CTreeltem For example, the CLi stBox class provides a set of inline wrapper functions, one per LB_XXX message. The ones that would be useful for the example are shown here. template class CLIstBoxT : public Base { public: m m m // for single-selection list boxes int C«tCurS€l() const { return (int)::SendMessage(m»hWnd, LB—GETCURSEL, 0, 0L); > int S«tCurS€l(int nSelect) | { return (int)::SendMessage(nuhWnd, LB_SETCURSEL, nSelect, 0L); } • • • // Operations // manipulating list box items int AddStringCLPCTSTR lpszlte*) { return (int):: SendMessageCfruhWnd, LB_A00STRINC, 0, (LPARAM) 1 psz I tem); } i ; m 」 9As of Visual C+ + 6.0,you'll find the a t l c o n tr o l s . h file on the library edition of MSDN, CD-I, in the \samples\vc98\atl\aticon directory. WINDOWING .. 灰 n .v). . • :• • • // selection helpers 1nt F1ndStr1ng(int nStartAfter, LPCTSTR lpszltem) const { return (Int)::SendMessage(nuhWnd, LB.FINDSTRING, nStartAfter, (LPARAM)lpszltem); } typedef CLi stBoxT CLIstBox; Assuming a data member of type ATLControl s :: CLi stBox, the updated example dialog code can now look much more pleasing: LRESULT CSt ri ngLi stDig::Onln i tDi alog(UINT, WPARAM, LPARAM, BOOL&) { CenterWindow(); II Cache list box HWND m_lb•Attach(CetDlgltera(IDC—LIST)); // Populate the 11st box ■Ub.AddStringC_T("Hello, ATL")); __1b.AddString(__T("AirTt ATL Cool?")); ■Jlb.AddStringC___T("ATL 1s your friend")); // Set initial selection int n - ■.Ib.FlrutStrlngCO, n_sz); i f ( n =» LB一 ERR ) n = 0; ^.Ib.SetCurSel(n); return 1; // Let dialog manager set initial focus } LRESULT CStringListDlg::OnOK(WORD, UINT, HWND, BOOLA) { // Copy the selected itea int n * b.GetCurSelC); if( n =« LB.ERR ) n 蓮 0; Ob.CetTextCn, «_sz); EndDialog(IDOK); return 0; 468 ATL INTERNALS Since the Windows control wrappers are merely a collection of inline functions that call SendMessage for you, the generated code is the same. The good news, of course, is that you don’t have to pack the messages yourself. These classes turn out to be so useful that I’d be very surprised if they didn’t turn up as supported, docu­ mented classes in future versions of ATL. However, because they are unsupported and undocumented, be prepared for the names and/or functionality to change. CContainedWindow The Parent Handling the Messages of the Child A CWi ndow-based object lets an existing window class handle its messages. This is useful for wrapping child windows. A CWi ndowlmpl-based object handles its own messages via a message map. This is handy for creating parent windows. Objects of another ATL window class, CContai nedWi ndow, let its parent handle the mes­ sages, letting an existing window class handle the messages the parent passes through. This is used to centralize message handling in a parent window. The par­ ent window can either create an instance of the child window class or it can let someone else create it, for example, the dialog manager, and subclass it later (1*11 discuss subclassing soon). Either way, the messages of the child window will be routed through the message map of the parent window. How does the parent win­ dow discern its own messages from those of one or more children? Alternate mes­ sage maps. Each CContai nedWi ndow will be given a message map ID and its messages will be routed to that alternate part of the parent’s message map. To support both creation of contained windows and subclassing of windows, CContai nedWi ndow is defined as follows: template class CContai nedWi ndowT : public TBase { public: CWndProcThunk nuthunk; LPCTSTR pszCIassName; WNDPROC m.pfnSuperWi ndowProc ; CMessageMap* wupObject; DWORD nudwMsgMapIO; const MSG* m^pCurrentMsg; // If you use this constructor you must supply // the Wi ndow Class Name, Object* and Message Map ID WINDOWING 4 6 9 // later to the Create call CContai nedWi ndowT(); CContainedWindowTCLPTSTR IpszClassName. CHessageMap* pObject, DWORD dwMsgMapID * 0); CContainedWindoivT(CMessageMap* pObject, DWORD dwMsgMapIO « 0); void SwitchHessageMap(DWORD dwMsgMapID); const MSG* GetCurrentMessa9e () const; LRESULT OefWindowProcC); LRESULT DefWindowProc(UINT uMsg, WPARAM wParam, LPARAM IParam); static LRESULT CALLBACK StartWi ndowProc(HWND hWnd( UINT LPARAM IParam); static LRESULT CALLBACK Wi ndowProc(HWNO hWnd» UINT uMsg ATOM Regi sterWndSuperclass(); HWNO Create(CMessageMap* pObject, RECT* prcPos, LPCTSTR uMsg, WPARAM wParam, WPARAM wParam, LPARAM IParam); DWORD dwWsgMapIO, HWNO hWndParent szWindowName * NULL. DWORD dwStyle - 0, DWORD dwExStyle • 0, UINT nID « 0, LPVOID lpCreateParam * NULL); HWND Create(LPCTSTR IpszClassName, CMessageMap* pObject, DWORD dv/lsgMapIO, HWND HWndParent, RECT* prcPos LPCTSTR szWindowName « NULL, DWORD dwStyle • 0, DWORD dwExStyle » 0 , UINT nIO « 0 f LPVOID lpCreateParam * NULL); // This function is deprecated; use the version // that takes a RECT* instead " : HWND Create(HWND HWndParent, RECTA rcPo^, > LPCTSTR szWindowName * NULL* DWORD dwStyle - 0, DWORD dwExStyle - 0, UINT nIO * 0, LPVOID lpCreateParam NULL) HWNO Cr«*t«(HWNO HWndParent, RECT* prcPos, LPCTSTR szWindowName « NULL, DWORD dwStyle * 0, DWORD dwExStyle UINT nIO * 0, LPVOID lpCreateParam NULL) 470 ATL INTERNALS BOOL HWND Subc1assW1ndoi«(HWND hWnd); UnsubclassW1ndow(BOOL bForce ■ FALSE); typedef CContainedWindowT Notice that CContai nedWi ndow does not derive from CWi ndowlmpl, nor does it derive from CMessageMap. CContai nedWi ndow objects do not have their own message map. Instead, the Wi ndowProc static member function of CContai ned­ Wi ndow routes messages to the parent window. The specific message m^> ID is pro­ vided either to the constructor or to the Create function. Creating Contained Windows Notice also the various constructors and Create functions, some taking the name of the window class and some not If you’re going to create an instance of 汪 CCon­ t a i nedWi ndow instead of subclassing, you have quite a bit of flexibility. For ex­ ample, to morph the letter box example to create an edit control that accepts only letters, CContai nedWi ndow would be used like this: class CMainWindow : public CWi ndowlmpl { public: ♦參• BECIN_MS(LMAP (CMai nWi ndow) • • • // Handl# the child edit control's Messages ALT.MSCLHAPC1) MESSAGEJiAN0LER0#LCHAR t OnEdl tChar) END_WSC_MAP() LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&) { // CrMttt th« contained window, routing Its m s sages to us if( n_«d1t.Cr«at_("ed1t", this, 1, ICWI ndoM::rctefault) ) { return 0; > return -1; } // L«t the cMId edit control receive only letters LRESULT 0nEd1tCh«r(UINTv WPARAM Mp«rw, LPARAM, B00U bHandled) { 1fC 1sa1pluiCCTa«AR)Hpar«i) ) bHwidlMl _ FALSE; WINDOWING 471 else MessageBeepCOxFFFFFFFF); return 0; } • • • private: CContai nedWi ndow m_edit; When the main window is created, this code associates an HWND with the CCon­ t a i nedWi ndow object m一 edi t by creating a new edit control, so identified because of the window class passed as the first parameter to Create. The second parame­ ter is the CMessageMap pointer for where the contained window’s messages are going to be routed. This parameter is most often the parent window, but doesn’t have to be. To separate the parent’s messages from the child’s, the parent's message map is broken up into two parts: the main part and one alternate part. The ID of the alter­ nate {3art of the message map is passed as the third parameter to Create. Finally, to filter out all characters but the letters sent to the contained edit con­ trol, the WM—CHAR handler only passes through the WM_CHAR messages that contain letters. By setting bHandled to FALSE, the parent window is indicating to the CContai nedWi ndow window procedure that it should keep looking for a handler for this message. Eventually, after the message map has been exhausted (including any chaining that may be going on), the Wi ndowProc passes the message onto the edit control’s normal window procedure. For nonletters, the WM_CHAR handler leaves bHandled set to TRUE (the default), which stops the message from going anywhere else and stops the child edit control from seeing any WM__CHAR messages without letters in them. As far as the child edit control is concerned, the user en­ tered only letters. Subclassing Contained Windows If you’d like to contain a child control that has already been created (for example, the dialog manager that has already created an edit control), you'll have to sub­ class 10 it. Previously in this chapter, I described superclassing as the Windows ver­ sion of inheritance for window classes. Subclassing is a much more modest and much more used technique. Instead of creating a whole new window class, with sub­ classing we merely hijack the messages of a single window. Subclassing is accom­ plished by creating a window of a certain class and replacing its window procedure with our own using SetWi ndowLong (GWL—WNDPROC). The replacement window 10 For a more complete dissection of Windows subclassing, see Win32 Programming, by Brent Rector and Joe Newcomer (Addison-Wesley, 1997). 472 ATL INTERNALS procedure gets all the messages first and can decide whether to let the original win­ dow procedure handle it as well. If you think of superclassing as specialization of a class, subclassing is specialization of a single instance. Subclassing is usually per­ formed on child windows, for example, an edit box that the dialog wishes to restrict to letters only. The dialog would subclass the child edit control during WM^INIT- DIALOG and handle WM_CHAR messages, throwing out any that weren't suitable. For example, subclassing an edit control in a dialog would look like this: class CLettersDIg : public CDialogImpl { p u b l i c : // Set the CMessageMap^ and the message map ID CLettersDlgO : m_edit(this, 1) {} BECIN.MSC_MAP(CLettersD1g) • • • ALT_MSG—MAP(l) MESSACE_HANDLERCWM_CHAR, OnEditChar) END_MSG_MAP() enum { IDD = IDD_LETTERS_ONLY }; LRESULT OnlnitDialogCUINT, WPARAM, LPARAM, BOOL&) { // Subclass the existing child edit control n_edit.SubclassWindow(CetDlgItem(IDC_EDIT)); return 1; // Let the dialog manager set the initial focus private: CContai nedWi nciow m 一 edit; In this case, because the example doesn't call C reate, it has to pass the CMes- sageMap pointer and the message map ID of the child edit control in the construc­ tor using the member initialization list syntax. It would seem that the ATL folks could've had one more version of SubclassWi ndow, since they had so many ver­ sions of Create, but maybe they ran out of bits. Anyway, once the contained win­ dow knows to whom to route the messages and to which part of the message map, it only needs to know the HWND of the window to subclass. It gets this in the Sub- c l assWi ndow call in the WM—INITDIALOG handler. Uow subclassing is performed is shown in the CContai nedWi ndow implemen­ tation ofSubclassWindow : WINDOWING template BOOL CContai nedWi ndowT::SubclassWindow(HWNO hWnd){ «L.thunk.Init(WindowProc, this); WNDPROC pProc - (WNOPROC)Am^thunlc. thunk; WNOPROC pfnWndProc « (WNDPROQ:: SetWi ndo«rLong(hWmif CWL.WNOPROC, CLONC)pProc); if (pfnWndProc — NULL) return FALSE; m-pfnSuperWindowProc _ pfnWndProc; m_hWnd » HWnd; return TRUE; } The important part is the call to SetWi ndowLong passing CWL一 WNDPROC This re­ places the current window object’s window procedure with an ATL thunking ver­ sion that routes messages to the container. It also returns the existing window procedure, which CContai nedWi ndow caches to call for any messages the con­ tainer doesn’t handle. Containing the Windows Control Wrappers After being introduced to the ATL Windows control wrsqpper classes (such as CEdi t, CLi stBox, etc.), you may dread window containment. If CContai ned­ Wi ndow derives from CWi ndow, where do all the nifty inline wrapper functions come from? Never fear, ATL is here. As you’ve already seen, CContai nedWi ndow is really just a type definition for the CContai nedWi ndowT template class. One of the parameters is a window traits class, which won’t help you. The other, however, is the name of the base class. CContai nedWi ndow uses CWi ndow as the base for CContai nedWi ndowT, but there’s no reason you have to. By using one of the ATL Dase the J Windows control wrapper classes instead, you can have a contained window that also has all the wrapper’s functions. For example, we can change the type of the nuedi t variable like so: CContai nedWi ndowT nuedi t; This technique is especially handy when you’re using C re ate instead of Sub- cl assWi ndow. With Create, you have to provide the name of the window class. If you call Create without a window class, the CContai nedWi ndow object attempts to acquire a window class by calling the base class function GetWndClassName, which is implemented by CWi ndow like so: static LPCTSTR CWi ndow: :CetWndClassNaiie() { return NULL; > 474 ATL INTERNALS However, each of the ATL Windows control wrappers overrides this function to pro­ vide its window class name. For example: static LPCTSTR CEditT: :CetWndaassName() { return _T(”EDrr); > Now, when creating an instance of one of the contained window wrapper classes, you don’t have to dig through the documentation to figure out what the class name is of your favorite intrinsic window class, you can simply call Create: class CMainWindow : public CWindowlmpl<...> { • • • LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&) { // Window cl ass na«e provided by base class ifC m_ed11 .Create(this, 1, n^hWnd, ACWindow::rcDefault) ) { return 0; } return -1; } • • ■ private: CContainedW1ndowT «.edit; Summary ATL takes the same principles of efficiency and flexibility that were originally de­ veloped for COM and applies them to another part of the Windows API, User32. This support takes the form of a small set of windowing classes. CWi ndow, which forms the root class of the windowing hierarchy, provides a large number of inline wrap­ per functions for manipulating an existing HWND. To create a new window class or to superclass an existing one and to handle messages from windows of that class, ATL provides CWi ndowlmpl. CDi a l oglmpl provides the same functionality for dialog boxes, both modal and modeless. To subclass child windows and manage messages in the parent, ATL provides CContai nedWi ndow. These classes can be used in standalone applications or in COM servers. For COM servers that expose COM controls, the windowing classes form the core of how input and output arc managed, as discussed in the following ch^)ter. CHAPTER 10 ActiveX Controls A Review of ActiveX Controls A complete review of the COM interfaces and interactions between an ActiveX control and a control container is outside the scope of this book. If you are unfa­ miliar with the various interfaces and interactions described in this chapter, there arc various other texts that specifically address these topics. In sid e O LE by Kraig Brockschmidt (Microsoft Press, 1995) is the original COM text and devotes hun­ dreds of pages to in-place activation and visual interface components. An ActiveX control is a superset of an in-place activated object, so you'll also need to read the OLE controls specification from Microsoft, which describes the re­ quirements of a control. In addition, the OLE controls 1996 specification, commonly referred to as the OC96 spec, documents optimizations for control activations (such as windowless controls and windowless control containment), two-pass rendering for nonrectangular windows, hit testing for nonrectangular windows, anci fast ac­ tivation protocols between controls and containers, as well as numerous other features. So, rather than rewording the material available in the references just de­ scribed, I’m going to show you how to implement such an object. This chapter de­ scribes how to implement a feature-complete ActiveX control using ATL. ActiveX Control Functionality A control incorporates much of the functionality you’ve seen in prior chapters. For example, a control is a COM object. Therefore, an ATL control contains all the standard functionality of an ATI,based COM object A control is a user-interface (UI) component; therefore it has thread affinity and should live in a single-threaded apartment. A control thus derives from the CComObjectRootEx base class. A control must be a createable class so its container can instantiate it. There­ fore, the control class will also derive from CComCoClass. Many controls use the CComCoCI ass default class object’s implementation of the IClassFactory in­ terface. Licensed controls override this default by specifying the DECLARE_CLASS- FACT0RY2 macro, which declares a class object that implements the IClass- Factory2 interface. 475 476 AT L INTERNALS In addition, most controls will support one or more of the following features: ■ Stock properties and methods such as ForeColor and Refresh that a con­ tainer can access via the control’s ID i spatch implementation. ■ Custom properties and methods that a container can access via the control’s IDi spatch implementation. ■ Stock and custom event callback methods using the connection points proto­ col to a container’s di spi nterface implementation. This requires the con­ trol to implement the IConnecti onPoi ntContai ner and IProvi deCI ass- Info2 interfaces as well as a connection point that makes calls to the event dispinterface. ■ Property change notifications to one or more clients using the connection points protocol to the clients’ I Property Not i fyS i nk interface implementa­ tions. Control properties that send such change notifications should be marked in the control’s type library using the bi ndable and/or requestedi t attri­ butes, as appropriate. ■ On-demand rendering of a view of the object via the IViewObject, IView- 0bject2, and IVi ewObjectEx interfaces. ■ Standard OLE control functionality as provided by the 101 eControl interface and in-place activation using the IO le O b je c t and IO le ln P la c e A c tiv e - Object interfaces. ■ Persistence support for various containers. At a minimum, a control typically provides support so that a container can save the object into a stream using the IPersi stStream lni t interface. Many controls will additionally support per­ sistence to a property bag using IP ersi stPropertyBag because Visual Basic and Internet Explorer prefer this medium. Some controls additionally support IP ersi st Storage so they can be embedded into OLE documents. ■ Fast and efficient windowless activation as provided by the 101 e l n PI ace- ObjectWi ndowl ess interface when the control’s container supports this opti­ mization. ■ Fast and efficient exchange of multiple interfaces during activation between a control and its controls using the IQui ckActi vate interface. ■ Object safety settings either through component category membership or via IObjectSafety. ■ Drag-and-drop support as provided by implementations of the ID ataObject, IDropSource, and IDropTarget interfaces. ■ A graphical user interface that provides a means to edit the control’s properties. Typically a control provides one or more COM objects, called property pages, ACT 丨 VEX CONTROLS 477 each of which displays a user interface that can modify a logically related sub­ set of the control’s properties. A container requests the CLSIDs of the property page COM objects using the control’s ISpecifyPropertyPages interface implementation. ■ A container that can access information about the properties of a control that supports property pages by using the IPerPropertyBrowsing interface. For example, the container can obtain a text string describing a property, determine which property page contains the user interface to edit the property, and re­ trieve a list of strings describing the allowed values for the property. ■ Support for arranging the control’s properties by category in Visual Basic’s property view. A control implements the IC a te g o r iz e P r o p e r ti es interface to provide the list of categories to Visual Basic and to map each property to a category. ■ Default keyboard handling for an ActiveX control. This is commonly needed for tabbing, default button press on Enter, arrow keys, and popping up help. ■ Setting the Mi scS tatus flags for a control. Special settings are necessary for some controls to operate properly. Property Page Functionality Because a control frequently provides one or more property pages, a complete con­ trol implementation will also supply one or more property page objects, which each ■ Implement (at least) the IPropertyPage interface, which provides the main features of a property page object. ■ Optionally implement the IPropertyPage2 interface to support selection of a specific property. Visual Basic uses this support to open the correct property page and set the input focus directly to the specified control when the user wants to edit a property. ■ Receive property change notifications from one or more controls using the con­ nection points protocol to the property page’s IPropertyN oti fySi nk inter­ face implementation. The BullsEye Control Requirements This chapter describes the ATL implementation of a feature-rich control called BullsEye. The BullsEye control implements all the previously described features. The BullsEye control draws a bulls-eye. You can configure the number of rings in the 478 ATL INTERNALS bulls-eye (from one to nine) and the color of the center ring as well as the color of the ring acljacent to the center (called the al­ ternate ring color). BullsEye draws additional rings by alter­ nately using the center and alternate colors. The area around the bulls-eye can be transparent or opaque. When transparent, the background around the bulls-eye shows through. When opaque, the bulls-eye fills the area around the circle using the back­ ground color. By default, BullsEye uses the container’s ambient background color as the background color. BullsEye also uses the foreground color to draw a line separating each ring. You can assign score values to each ring. By default, the center ring is worth 512 points and each other ring is worth half the points of its adjacent inner ring. When a user clicks on a ring, the control fires an OnRi ngHi t event and anOn- ScoreChanged event. The argument to the OnRi ngHi t event method specifies the ring upon which the user clicked. Rings are numbered from I to N f where 1 is the centermost ring. The OnScoreChanged event specifies the point value of the clicked ring. For example, clicking on ring 2 with default scores fires an On- ScoreChanged event with an argument of 256 points. In addition, when you click on one of the bulls-eye rings, the control can pro­ vide feedback by playing a sound. By default, you hear the sound of an arrow strik­ ing the bulls-eye. The Boolean Be^p property, when set to TRUE, indicates the control should play its sound on a ring hit. BullsEye supports all standard control functionality. In addition to windowed activation, BullsEye can be activated as a windowless control when its container supports such functionality. Many containers ask their controls to save their state using the IP ersi st- S tream lnit interface and an IStream medium. When embedding a control in an OLE document, a container asks a control to save its state using the IP ersi st- Storage interface and the IStorage medium. A container, such as Internet Ex­ plorer and Visual Basic, that prefers to save the state of a control as textual name/ value pairs uses the control’s IP ersi stPropertyBag interface and the IProper- tyBag medium. BullsEye supports all three persistence protocols and media— streams, storages, and property bags. BuilsEye also provides two property pages. One property page is custom to the BullsEye control and allows you to set the Enabl ed, Beep (sound on ring hit), and BackStyl e (transparent) properties (Figure 10.1). The other is the standard color selection property page (Figure 10.2). The BullsEye control has four color proper­ ties: the center ring color, the alternate ring color, the background color (used to fill the area around the bulls-eye) and the foreground color (used to draw the separa­ tor line between rings). AJoSa^o joiAHqag pjepuF^s aifi ui Xyadojd dddg sn sjsn ioi^uod aqx itioga^o aoirejBaddy pjBpuFjs 9 ^ ui X^iddojd auno36uL^ ai^ pire sapj^doid jojod s^\ sasjl Iojiuod aXgsnng aqx (8 0T 9Jn3ijL) sauoga^D paugap-joxjuoD pire pjepirejs Xq Sdi^iddojd 3叩 SJJOS M8IA B p a p s UBO HOX 1{DII{AV. UI MOpUlM M01A X^iadOJd B SBl{ OISBQ [Bns!八 -disbq [BnsiA jo j soi^M^dojd sji sdzuoSa^o o s]B pjiuoD a^gsnng aqx a相d iCM3dojd jo | o d eXgsung -z'O !• 9Jn6y ■■■__■..... ,■■___ g a d o id uio^sno a^asung • |/o !• OJnGy Ti w x i r «> f ? S | jfluppM^N Wfiu«i«punc»s £ j j | U M K J R » U 」 P » f l ~ 3 £i …. --MVlin SOfX?H AlJcXlOJd si)(x、d iyffl 6 lp SlObJ 丄 NOO X3 八丨丄 CIV 480 ATL INTERNALS ^ • BultsEypl n |BullsEyel BullsEye ] m m it I — . 一‘ J AlternateColor ■ 0 BackCoior □ 2147483663 CenterColor 1 255 ForeColor _ 2147483666 RingCount 5 日 Behavior CausesValidation Draglcon Iti^be mute . | DragMode 0 • vbManual Tablndex 0 TabStop True Visible True Misc (About) (Custom) (Name) BulkEyel BackStyte Transparent Enabled True HelpContextID 0 Index Tag ToolTipText WhatsThisHelpID 0 Position Height 2775 Left 960 Figure 10.3. Visual Basic property view window for BullsEye BullsEye also supports per-property browsing. Per-property browsing allows a control to specify a list of strings that a container should display as the available choices for a property’s value. Notice in the example Visual Basic property view window that, in the Behavior category, Visual Basic displays the strings “Yes, make noise” and MNo, be mute” as the selections available for the Beep property. WINDOWING the data at all, making the complaint dialog unnecessary. The following updated code shows this technique: class CStringOlg : public CDialoglmpl { public: 曠參i BEGIN.MSC.WAP(CSt ringDlg) • • • COMMANO.HANDLERCIDCLSTRINGf EN^CHANCE, OnStringChange) END_MSC_MAP() private: void CheckValidStri ng() { // Check the length of the string int cchString • ::GetWindowTextLength(GetDlgItem(IDC—STRING)); // Enable the OK button only if the string is of nonzero length ::Enab1eWindowCGetD1gIteni(IDOK), cchString ? TRUE : FALSE); LRESULT OnlnitDialogCUINT, WPARAM, LPARAM, BOOL&) { CenterWindow(); II Copy the string from the data member to the child control (DOX) SetDl gltemText (IDC_STRINC, i s z ) ; // Check the string length (DDV) CheckVal idStringO; return 1; // Let dialog manager set initial focus } LRESULT OnStringChange(WORD, UINT. HWND, BOOL&) { " Check the string length each time it changes (DOV) CheckValidStringC) ; return 0; } … // The rest is the same In this case, notice that O nlnitD i alog takes on some DDV responsibilities, while OnOK loses some. In O nlni tD i al og, if the string starts with a zero length, the OK ATL INTERNALS Table 10.3. BullsEye Custom Properties Property name Type Custom DISPID Description A p p li c a tio n IDispatch* DISPIDJVPPLICATION Return the IDi spatch* for the hosting appli­ cation Alternate- OLE一 COLOR DISPID_ALTERNATE- Get/set the color of the C olor COLOR alternate (even) rings Beep VARIANT一 BOOL DISPID_BEEP Enable/disable sound effects for the control C e n te rC o lo r OLE^COLOR DISPID—CENTERCOLOR Get/set the color of the center (odd) rings P arent IDispatch* DISPID_PARENT Return the IDi spatch* for the control’s parent R ingC ount s h o rt DISPID 一RINGCOUNT Get/set the number of rings R ingV alue long DISPID—RINGVALUE Get/set the value of each ring Declaring the Properties and Methods in IDL A control container accesses the propert ies and methods of a control using the con- trol’s ID i spatch interface. A control niust therefore provide an implementation of ID i spatch when it has properties and methods. ATL-based controls, in general, and the BullsEye control, specifically, imple­ ment their properties and methods using a dual interface, not a dispatch interface, even though a d u a l interface is unnecessary because the vtable port ion of the dual interface will typically go unused. A custom C + 十 control container could access the control's properties and methods using the vtable, but no other container cur­ rently does. At the time of this writing, even Visual Basic 6 accesses properties and methods of a control using the control’s ID i spatch interface. Visual Basic only uses the vtable portion of a dual interface for noncontrol objects. The BullsEye control provides access to its properties and methods on the de­ fault I Bui 1 sEye dual interface. When you generate a new ATL-based control class, the wizard generates the definition of the default dual interface, but you must populate the definition with the accessor methods for your control’s properties and the control’s methods. The definition of the IB ullsE ye interface is given in Listing 10.1. ACTIVEX CONTROLS 4 8 3 object, dual, pointer_default(unique), iiuid(7DC59CC4-36C0-llD2-AC05-00A0C9C8E50D), helpstring("IBullsEye Interface"), nterface IBullsEye : IDispatch const int DISPID_ALTERNATECOLOR = 1; const int const int DISPID_CENTERCOLOR = B; const int const int DISPIO_RINGVALUE » S; const int const int DISPID^PARENT - 7; #define PROPGET propget, bindable, requested!t #define PROPPUT propput, bindable, requestedit DISPID_BEEP DISPID_RINCCOUNT DISPIDJ\PPLICATION BackColor([in]0LE_C0L0R clr); BackColor BackStyle([in]long style); BackStyle([out,retval]long* ForeColor([in]0LE_C0L0R cl 「) ; ForeColor [PROPPUT, id(DISPID 一 BACKCOLOR)] HRESULT [PROPGET, id(DISPID_BACKCOLOR)] HRESULT ([out ,retval]OLE_COLOR*pclr); [PROPPUT, idCDISPID^BACKSTYLE)] HRESULT [PROPGET, id(DISPID.BACKSTYLE)] HRESULT style); [PROPPUT, id(DISPID 一 FORECOLOR)] HRESULT [PROPGET, id(DISPIO_FORECOLOR)] HRESULT ([out,retval]OLE一COLOR* pclr); [PROPPUT, id(DISPID_ENABLED)] HRESULT Enabled([in]VARIANT.BOOL vbool); [PROPGET, id(DISPID 一 ENABLED)] HRESULT Enabled ([out ,retval]VARIANT_BOOL* pbool); [PROPPUT, id(DISPID_ALTERNATECOLOR)] HRESULT ATternateColor([in] 0LE_C0L0R newVal); [PROPGET, id(DISPIDJVLTERNATECOLOR)] HRESULT AlternateColor([out, retval] OLE 一 COLOR *pVa1); [PROPPUT, id(DISPID_BEEP)] HRESULT Beep([in] VARIANT_BOOL newVal); [PROPGET, id(DISPID_BEEP)] HRESULT Beep([out, retval] VARIANT_BOOL ApVal); [PROPPUT, id(DISPID_CENTERCOLOR)] HRESULT CenterColor([in] OLE_COLOR newVal); [PROPGET, id(DISPID_CENTERCOLOR)] HRESULT Cente 「Color([out , retval] OLE 一 COLOR *pVal); [PROPPUT, id(DISPID—RINCCOUNT)] HRESULT RingCount([inJ short newVal); [PROPGET, id(DISPID_RINGCOUNT)] HRESULT RingCount([outt retval] short *pVal); Listing 10.1. The I Bui 1 sEye interface 484 ATL INTERNALS [PROPPUT, id(OISPIO_RINCVALUE)] HRESULT RingValue([in] short sRingNumber, [in] long newVal); [PROPGET, id(DISPIO^RINGVALUE)] HRESULT RingValue([in] short sRingNumber, [out, retval] long *pVal); [PROPGET, id(DISPID_APPLICATION)] HRESULT Application([out, retval] LPDISPATCH *pVal); [PROPGET, id(OISPID_PARENT)] HRESILT Parent([out, retval] LPDISPATCH *pVal); [id(DISPID_REFRESH)] HRESULT Refresh(); [id(DISPID_DOCLlCK)] HRESULT DoClick(); [id(OISPIDJ\BOUTBOX)] HRESULT AboutBox(); }; Requirements: The Events BullsEye Custom Events The BullsEye control doesn't support any of the stock events. However, it has two custom events, as detailed in Table 10.4. An event interface contains only methods and should be a dispatch interface in order for all containers to receive the event callbacks. Some containers,such as Visual Basic, can receive event callbacks on custom IUnknown-derived interfaces. An event interface should never be a dual interface. Table 10.4. BullsEye Custom Events Event void OnRingHit (short sRingNumber) void OnScoreChanged (1r,ng Ri ngVal ue) Event DISPID DISPID_ONRINGHIT DISPID_ONSCORECHANGED Description The user clicked on one of the bulls-eye rings. The argu­ ment specifies the ring that the user clicked. Rings are numbered from 1 to TV from the center outward. This event follows the On- Ri ngHi t event when the user clicks on a bulls-eye ring. The argument specifies the score value of the ring that the user clicked. ACTIVEX CONTROLS 485 Declaring the Event Dispatch Interface in IDL The definition of the _IB ul 1 sEyeEvents dispatch interface is shown in Listing 10.2. In order for the constants for the DISPIDs to appear in the MIDL-generated C/C+ + header file, the definitions of the constants must appear in the IDL file out­ side of the library block. You must define the di spi nterface itself inside the li­ brary block. Listing 10.2. The _IB ul 1 sEyeEvents dispatch interface const int DISPID_ONRINCHIT = 1; const int DISPID.ONSCORECHANGED = 2; uuid(19FF9872-36ED-lld2-ACOS-00A0C9C8E50D), helpstringC'Event interface for BullsEye Control") ] dispinterface _IBu11sEyeEvents { properties: methods: [id(DISPID_ONRINCHIT)] void OnRingHit(short ringNumber); [id(DISPID_ONSCORECHANGED)] void OnScoreChanged(long ringValue); Requirements: The BullsEye and Property Page Coclasses Declaring the BullsEye Class and Its Property Page Class in IDL You must also define the BullsEye cocl ass in the library block of the IDL file (List­ ing 10.3). At a minimum, you must specify the default IDispatch interface (IB ul 1 sEye), via which a container can access the control’s properties and meth­ ods, and the default source interface (一IBul 1 sEyeEvents), through which the BullsEye control fires events to its container. Listing 10.3. The BullsEye cocl ass [ uuid(7DC59CC5-36C0-llD2-AC05-00A0C9C8E50D) , helpstri ng("Bui1sEye Class") ] coclass BullsEye { 4 8 6 ATL INTERNALS [default] interface IBullsEye; [default, source] di spi nterface _IBul1sEyeEvents; }; Additionally, you should define all the custom properly page classes imple­ mented by your control in the library block of the IDL file. BullsEye only has one custom property page, called Bui 1 sEyePropPage (Listing 10.4). Listing 10.4. The Bui 1 sEyePropPage cocl ass [ uuid(7DC59CC8-36C0-HD2-ACO5-00AOC9C8E50D), helpstringC'BullsEyePropPage Cl ass") ] coclass BullsEyePropPage { interface IUnknown; Creating the Initial Control Using the ATL Wizard Some people prefer to write all the code for a control by hand. They don’t care for the wizard-generated code because they don’t understand what it does and doesn’t do. Occasionally, the wizard-generated code is incorrect, as well. Even if you gen­ erate the initial code base using the wizard, you will change it greatly before the control is complete anyway, so you might as well save some time and effort initially by using the wizard. Selecting Options for the CBullsEye Implementation Class I took the requirements for the BullsEye control, used the new ATL Object Wizard, and requested a Full Control because it is the option most like my requirements. Here are my responses to the wizard dialogs. First I defined the name of my implementation class, CBul 1 sEye, its source file names, the primary interface name (IB ui 1 sEye) and various COM object registra­ tion information (Figure 10.5). Then I specified various COM object options for the control (Figure 10.6). Controls need thread affinity (because they are UI components and use window handles, which are associated with a thread). Therefore the wizard correctly only allows you to request the Single or Apartment threading models for a control. Containers will access a control’s properties and methods using the control’s ID i spatch interface. The easiest way to get an ID i spatch implementation in your ACTIVEX CONTROLS 487 ATI Ol)j(Ht W iAvd Properties n r ? O m : |CBu»sEye •CEP O k BJI^Eye.h BullsEye. cpp CflOeerfBukFye J y p y jBuisEye Cl«s Progp [ATLIn!em^Bul$£v Figure 10.5. BullsEye Names dialog All ()!>jone 厂 AdtBwbuHon AfiU 1 •_ ftalobai 厂货MowedQf# P tnm ndlt OK Figure 10.7. Miscellaneous control options for the BullsEye control The Miscellaneous page allows you to select various control options not avail­ able elsewhere. The only options that apply to the BullsEye control are the ones se­ lected in the dialog shown in Figure 10.7. Select the Opaque option when your control is completely opaque and none of the container shows through within the control’s boundaries. This helps the container draw the control more quickly. An opaque control can also specify that the background is a solid color, not a patterned brush. Ill discuss how to implement the BullsEye rendering code so that it supports transparent areas around the bulls-eye, but let’s start with an opaque control. The Opaque and Solid Background options simply specify the flags used in the DE- CLARE_VIEW_STATUS macro. When selected, the Normalize DC (device context) option causes your control to override the OnDraw method for its rendering. When not selected, the control overrides the OnDrawAdvanced method by default. OnDrawAdvanced saves the state of the device context, switches to MM—TEXT mapping mode, calls OnDraw, and then restores the saved device context. Therefore, when you ask for a normalized DC and don’t override OnDrawAdvanced, you introduce a little more overhead. BullsEye uses this support, though. The Insertable option allows the control to be embedded by any application that supports embedded objects through the Insert Object system dialog. Microsoft Word and Excel are two such applications. Selecting this option causes, among other things, your control to support the IPersi stStorage and IDataObject interfaces. Finally, you can have the wizard generate support for any stock properties you want the control to support. BullsEye requires four stock properties, which I’ve se­ lected in the dialog (Figure 10.8). You'll have to slightly enhance the IDL for the ACTIVEX CONTROLS AM C)l)ieit W»z*rdProperties N«nic| AMbUhMj Stock P rap «ttM Bordet Color i I ^bB8 Iz i ^ < J $ke Bonier Cotor Bordef Styte Borde» Visfcte B a dw W rth Caption Draw Mode ■ i »_• ■'■•V A ii Background Sty4e Enabted Fcxegiound Cokx BB Figure 10.8. Stock properties for the BullsEye control stock properties, but ATL provides the implementation of the property accessor methods. There is no wizard support for stock methods, so you’ll have to implement them, as well as BullsEye’s custom properties, by hand. Base Classes Used by Wizard-Generated Classes The ATL wizard generates the initial source code for a control. The control class de­ rives from a number of different base classes, depending on the type of control you ask the wizard to generate. The wizard also adds support for various features based on various selections you make from the ATL Object Wizard Properties dialog pages. Table 10.5 summarizes the base classes used by the different types of wizard- generated controls. The Initial BullsEye Source Files The Initial CBullsEye Class Declaration The initial wizard-generated class declaration for the CBul 1 sEye control class is shown in Listing 10.5. I’ll have to make a number of changes to the class before it meets all the requirements described at the beginning of the chapter. For example, I’ll need to add a few more base classes to obtain all the required control function- aJity. Also, presently there are no properties supported by the control except the stock properties I selected via the wizard dialogs. Plus, there is quite of bit of im­ plementation code to write to make the control draw and behave as a bulls-eye. Fll get to all that, but first, let’s look at the initial source code. Table 10.5. Base Classes Used by Various Control Types Base classes used Full control Lite control Composite control HTML control Lite com­ posite control Lite HTML control CComObjectRootEx CComCoClass CComControl 101eControlImpl 101elnPlaceActi veObjectlmpl 101elnPlaceObjectWi ndowlesslmpl IOleObjectlmpl IPersistStoragelmp! IPersistStreamlnitlmpl IPropertyNotifySinkCP IProvideCIassInfo2Impl IQuickActivatelmpl ISpecifyPropertyPagesImpl ISupportErrorlnfoImpl IViewObjectExImpl Control uses normalized DC Control is windowed only CPE User User User User CComCompositeControl CStockPropImpl SP SP SP IConnectionPointContainerlmpl CPE CPE CFE IDataObjectlmpl IDispatchlmpl No SP No SP No SP &I)uaJ & Dual &DuaI CPE SP CPE CPE User SP CPE No SP & Dual SP CPE SEI User SP - Stock properties selected; CPE property page. Connection points enabled; SEI = Support Error Info enabled; User - User selection on wizard 490 ATL INTERNALS ACTIVEX CONTROLS I’ve reformatted the source code slightly from the original wizard-generated source codc to group related functionality together and to add a few comments. Listing 10.5. The initial wizard-generated CBull sEye class ///ft///////(/////////////////////i/i////////////!//////////////I////// // CBullsEye class ATINO^VTABLE CBullsEye : // COM object support public CComObjectRootEx, // Class object support public CComCoClass, // Error info support for default dual interface public ISupportErrorlnfo, // 8asic ”Lite" control implementation public CComControlf public 101eControlImpl, public 101eObjectImpl, public 101elnPIaceActiveObjectlmpl , public IOlelnPlaceObjectWindowlesslmpl, public IViewObjectExImpl, // "Lite" control persistence implementation public IPersistStreamlnitlmpl, // Ful1 control additional implementations // Support for OLE embedding public IDataObjectlmpl, public IPersistStoragelmpl, // Support for property pages public ISpecifyPropertyPagesImpl, // Support for fast activation public IQuickActivatelmpl, // Connection point implementation public IConnectionPointContai nerlmpl, public IProvideClassInfo2Impl<&CLSID_Bul1sEye• &DIID—IBul1sEyeEvents, &LIBID_ATLInternalsLib>, 492 ATL 丨 INTERNALS // Full controls supporting connection points // also receive property change notification support public IPropertyNotifySinkCP { p u b l i c : CBullsEyeO { } DECLARE-REGISTRY^RES0URCE1D(IDR„BJLLSEYE) DECLARE^PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CBul 1 sEye) // Default dual (IDispatch-derived) interface to control COM_INTERFACE_ENTRY(IBulIsEye) COM^INTERFACE_E^TRY(IOispatch) // Error info support for default dual interface COM_INTERFACE^EMTRY(ISupportErrorInfo) // Basic "Lite” control implementation COM.INT E R FAC E^ENTRY(101eControl) COM.INTERFACE^ENTRY(101eObj ect) COM—INTERFACE—ENTRY(101elnPlaceActiveObject) C0M_INTERFACE_ENTRY(I01eInPlace0bject) COM 一 INTERFACE—ENTTRY(IOlelnPlaceObjectWindowless) C0M_INTERFACE_ENTRY2(101eWindow , 101elnPlaceObjectWindowiess) COM_INTERFACE_ENTRY(IViewObjectEx) COM一 INTERFACE—ENTRY(IViewObj ec12) COM_INTERFACE_ENTRY(IViewObject) ,// "Lite" control persistence implementation COM一 INTERFACE—ENTRY(IPersistStreamlnit) C0M_INTERFACE_ENTRY2(IPersist, IPersistStreamlnit) // Full control additional implementations // Support for OLE embedding COM^INTERFACE_EMTRY(IDataObject) COM_INTERFACE_ENTRY(IPersistStorage) // Support for property pages COM_INTERFACE_ENTRY(ISpecifyPropertyPages) // Support for fast activation COM-INTERFACE_E^TRY(IQuickActivate) // Support for connection poi nts COM^INTERFACE_ENTRY(IConnectionPointContainer) COM_INTERFACE_E>JTRV (IProvi deCl assInfo2) COM一 INTERFACE—ENTRY(IProvideC) asslnfo) END 一 CONLMAPO ACTIVEX CONTROLS 493 // Initially, the control1s stock properties are the only // properties supported via persistence and property pages BEGIN—PROP_MAP(CBul1sEye) PROP_DATA_ENTRY("_cx”, m _sizeExtent. cx, VT_UI4) PROP_DATA_ENTRY("_cy", m _sizeExtent. cy, VT_UI4) PROP_ENTRY(MBackColorM, DISPID.BACKCOLOR, CLSID_StockColorPage) PROP^ENTRY(,,BackStyleH , DISPID_BACKSTVLE, CLSID—NULL) PROP_ENTRY("Enabledn , DISPID^ENABLED, CLSID_NULL) PROP_ENTRY("ForeColor", DISPID_FORECOLOR, CLSID.StockColorPage) END_PROP_MAP() H Initially, enabling connection poi nt support for a full control // only supports property change notifications BEGIN_CONNECTION^POINT_MAP(CBul1sEye) CCNNECTION_POINT_ENTRYCIID_IPropertyNotifySink) END_CONNECTION_POINT_MAP() // Initially, the control passes all Windows messages to the base class BEGIN_MSG_MAP(CBullsEye) CHAIN_MSC_MAP(CComControl) DEFAULT_REFLECTION_HANDLER() END_MSC_MAP() // ISupportErrorlnfo STDMETHOD(InterfaceSupportsErrorlnfo)(REFIIO ri i d) { II Implementation deleted for clarity. • • } // IViewObjectEx DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKCND 丨 VIEWSTATUS—OPAQUE) // IBullsEye public: HRESULT OnDraw(ATL_DRAWINFO& d i) {//••• Sample drawing code omitted for clarity } // Initially, the only member variables are those for the stock II properties 0LE_C0L0R m_clrBackColor; LONG m_nBackStyle; BOOL m_bEnabled; OLE一COLOR m_clrForeCo1or; 494 ATL INTERNALS The Initial IBullsEye Interface Listing 10.6 is the initial wizard-generated IDL description for the IB ullsEye in­ terface. The wizard generates the interface containing any stock properties you’ve specified. I’ll need to add all the custom properties for the control as well as any stock and custom methods supported by the control. Listing 10.6. The initial wizard-generated IDL for the I B u l 1 sEye interface [ object, dual uuid(7DCS9CC4-36CO-HD2-ACO5-0OA0C9C8E5OD)f helpstring(uIBunsEye Interface"), pointer—default(uni que) ] interface IBullsEye : IDispatch { [propput, id(0ISPID_BACKC0L0R)] HRESULT BackColo「([in]0LE一COLOR clr); [propget, i d(DISPID_BACKC0L0R)] HRESULT BackColor ([out,retval]0LE_C0L0R* pclr); [propput, id(DISPID_BACKSTYLE)] HRESULT BackStyle([in]long style); [propget, id(DISPID.BACKSTYLE)] HRESULT BackStyleC[out,retval]1ongA pstyle); [propput, id(DISPID.FORECOLOR)] HRESULT ForeColorC[in]OLE_C0L0R clr); [propget, id(DISPID 一 FORECOLOR)] HRESULT ForeColor ([out,retval]0LE_C0L0R* pcir); [propput, id(DISPID—ENABLED)] HRESULT Enab1ed([in]VARIANT_B00L vbool); [propget, id(DISPID_ENABLEO)] HRESULT Enabled([out,retval]VARIANT_BOOL* pbool); The Initial JBullsEyeEvents Dispatch Interface The initial 一 IB ul 1 sEyeEvents dispatch interface is empty (Listing 10.T). I’ll need to add the BullsEye custom events to the di spi nterface. When a control sup­ ports any of the stock events, you’d add them to the event interface as well. Listing 10.7. The initial wizard-generated IDL for the 一 IB u l lsEyeEvent dispatch interface [ uuid(19FF9872-36ED-lld2-AC05-00A0C9C8E50D), helpstring("Event interface for BullsEye Control") ] ACTIVEX CONTROLS 495 dispinterface _IBullsEyeEvents { properties: methods: Developing the BullsEye Control Step by Step Stock Properties and Methods Updating Stock Properties and Methods in the IDL Your IDL file describing the control’s default dispatch interface must contain an en­ try for each stock property accessor method and all stock methods you support. The ATL Object Wizard will generate these method definitions for those stock prop­ erties added using the ATL Object Wizard dialogs. Listing 10.6 shows the method defuiitions for the BullsEye properties. However, there are a couple of changes you must manually make to the method definitions. First,all stock properties should have the bi ndabl e and requested! t attributes. This is because the stock property put methods fire change notifications to a container before and after changing a property. Therefore, you need to change each method like this: // Original wizard-generated version [propput, id(DISPID_BACKCOLOR)] HRESULT BackColor([in]0LE_C0L0R clr); [propget, id(DISPID_BACKCOLOR)] HRESULT BackCo1or([out,retval]OLE_COLOR* pclr); // Corrected version [propput, bi ndable, requestedit, id(DISPID_BACKC0L0R)] HRESULT BackColor([in]0LE_C0L0R clr); [propget, bindable, requestedit, id(DISPID 一 BACKCOLOR)] HRESULT BackColor([out,retval]OLE_COLOR* pclr); This change has no effect on the way the control actually operates. However, the type library describing the control now more accurately describes the actual be­ havior of the control. The ATL Object Wizard currently has no support for stock methods. You’ll need to add any stock methods explicitly to the default dispatch interface definition in your IDL file. There are only three stock methods presently defined (AboutBox, ATL INTERNALS DoCl ic k , and R efresh), and BullsEye supports them all. So I’ve added the follow­ ing lines to the IB u ll sEye interface definition: [i d(DISPID_ABOUTBOX) ] HRESULT AboutBoxO; [id(DISPID_DOCLICK)] HRESULT DoClickO; [id(DISPID 一 REFRESH)] HRESULT RefreshO ; Listing 10.1, shown earlier in this chapter, contains the complete definition of the I Bui 1 sEye interface with all the above recommended changes. Implementing Stock Properties and Methods Using CStockPropImpl The CStockPropImpl class contains an implementation of every stock property you can choose from the Stock Properties tab in the ATL Object Wizard. A control derives from CStockPropImpl when it wants an implementation of any of the stock properties. The declaration of the template class looks like this: template < class T, class InterfaceName, const IID* piid, const CUID* plibid> class ATL_NO«VTABLE CStockPropImpl : public IOispatchlmpl< InterfaceName, piid, pi ibid > The class T parameter is the name of your control class. The InterfaceName pa­ rameter is the name of the dual interface defining the stock property propget and propput methods. The CStockPropImpl class implements these accessor meth­ ods. The p i i d parameter is a pointer to the IID for the InterfaceName interface. The pi ib id parameter is a pointer to the GUID of the type library that contains a description of the InterfaceName interface. The CBul 1 sEye class implements its stock properties using CStockPropImpl like this: class ATL_NO_VTABLE CBullsEye : public CStockPropImpl, The CStockPropImpl class contains an implementation of the property accessor (get and put) methods for all stock properties. These methods notify and synchro­ nize with the control’s container when any stock property changes. Most controls don’t need support for all the possible stock properties. How­ ever, the CStockPropImpl base class contains supporting code for all stock prop­ erties. This code needs a data member for each property. ATL expects your deriving ACTIVEX CONTROLS 497 Table 10.6. Stock Properties Supported by CStockPropImpl Stock property Data member Stock property Data member APPEARANCE AUTOSIZE BACKCOLOR BACKSTYLE B0RDERC0L0R BORDERSTYLE BORDERVISIBLE BORDERWIDTH CAPTION DRAWMODE DRAWSTYLE DRAWWIDTH m 一 nAppearance m_bAutoSize m_clrBackColor m_nBackStyle m_clrBorderColor m_nBorderStyle m_bBorderVisible m_nBorderWidth m_bstrCaption m__nDrawMode m_nDrawStyle m_nDrawWidth ENABLED FILLCOLOR FILLSTYLE FONT HWND MOUSEICON PICTURE READYSTATE TABSTOP TEXT VALID m一 bEnabled m_cl「FillC ol or m_nFi11 Style m—pF ont m一 clrForeColor m_pMouseIcon m一 nMousePoi nter m _ p P ic tu re m_nReadyState m _bTabStop m _b strT e x t m _b V alid class, the control class, to provide the data members for only the stock properties that your control supports. You must name these data members the exact same vari­ able name as used by the CStockPropImpl class. Table 10.6 lists the appropriate name for each supported stock property. The CStockPropImpl class contains references to all these member variables because it contains property accessor methods for all these properties. In order that a control need not allocate space for all properties when it only needs to sup­ port a few properties, the CComControl Base class defines a union of all these member variables. CComControl Base itself is the base class for CComControl, from which CBul 1 sEye derives. // m一 nFreezeEvents is the only one actually used int m_nFreezeEvents; // count of freezes versus thaws // These are here to make stock properties work IPictureDisp* m^pMouselcon; IPictureDisp* m_pPicture; IFontDisp* m—pFont; 0LE_C0L0R m_clrBackColor; 498 ATL INTERNALS OLE_COLOR m_clrBorderColor; OLE—COLOR m.clrFillColor; OLE«XOLOR nucl 「ForeColor; BSTR m—bstrText; BSTR m_bst「Caption; BOOL m_bValid; BOOL m_bTabStop; BOOL m_bBorderVisible; BOOL m_bEnabled; LONG m„nBack5tyTe; LONG m_nBorderStyle; LONG m_nBo rde rWi dth; LONG m_nDrawMode; LONG m_nOrawStyle; LONG m_nDrawWidth; LONG m_nFi HStyle; SHORT m_nAppearance; LONG m_nMousePointer; LONG m_nReadyState; The net result of this space optimization is this: When you add a member variable to your control class to hold a stock property and you misspell the member variable name, you will receive no compilation errors. The code in CStockPropIm pl simply references the field of the above anonymous union contained in your control s base class. Typically, this is not the behavior you want. The Object Wizard generates the proper member variable(s) in your control’s class when you add a stock property initially. For example, here are the member variables generated for the stock properties in the C B ullsE ye class. m_cl rBacFireOnRequestEdit(dispid) S一 FALSE) return S^FALSE; \ pT->m_##pname * pname; \ pT->m_bRequiresSave * TRUE; \ pT->Fi reOnChanged(dispid); \ pT->Fi reVi ewChange(); \ ATL INTERNALS pT->SendOnDataChange(NULL); \ return S_OK; \ HRESULT STDMETHODCALLTYPE get_##fname(type* p##pname) \ ATLTRACE2(atlTraceControlsf2l^T(,,CStockPropImpl::get_Xs\nw). \ JT(#fname)): \ T* pT - (T*) this; \ *p##pnane « pT->m_##pname : \ • return S 一 OK; \ First, notice that the put method fires an OnRequestEdi t and an OnChanged event notification to the control’s container before and after, respectively, changing the value of a stock property. This behavior is why I changed the IDL for the stock prop­ erties to add the bi ndable and requestedit attributes. Second, the put method fires the OnRequestEdi t and OnChanged events with­ out regard to a control’s freeze event count. When a control’s freeze event count (maintained in CComControl Base m the nunFreezeEvents member variable) is nonzero, a control should hold off firing events or discard them completely. The fail­ ure of the stock property put methods to obey this rule causes some containers to break. For example, the Test Container 叩 plication shipped with Visual C++ 6.0 crashes when a control fires change notifications in its Fi nal Construct method. A control should be able to call FreezeEvents(TRUE) in FinalConstructtodiS" able change notifications, initialize its properties using the put methods, and then call FreezeEvents (FALSE) to enable change notifications if they were previously enabled. It’s a minor point, but note that changing a stock property sets the m_bRe- qui resSave member variable of your class to TRUE. A control inherits this mem­ ber variable from the CComControl Base class, so you don’t explicitly provide it. Theoretically, though, you might want to support stock properties in noncontrols, for example, server-side components. Such an object would have to provide its own definition of this member variable. Occasionally, you’ll decide to support additional stock properties after creating the initial source code. The wizard lacks support for adding features to your class after the initial code generation, so you’ll have to make the previously described changes to your code manually. Finally, often youll want to do some work over and above that which the stock property put functions perform. For example, the CBul 1 sEye class needs to know whenever the background color changes so it can delete the old background brush and schedule the rendering logic to create a new background brush. To do this, you ACTIVEX CONTROLS have to override the put^BackColor method provided by the CStockPropImpl class. Basically, this means you often end up rewriting most, if not a ll of the stock property put methods. You can reuse the get methods, but they are, for the most part, trivial. It would be nice if future versions of the CStockPropImpl put methods called a pT->OnStockPropChanged member function, for example, OnBackColor- Changed, to notify their deriving class that a particular stock property has changed value. CStockPropImpl could provide empty inline functions for all the stock properties and, therefore, not incur any overhead for the function when it wasn’t overridden. Custom Properties and Methods Adding Custom Properties and Methods to the IDL In addition to any stock properties, your control s default dispatch interface must contain an entry for the property get and put methods for each custom control prop­ erty as well as all the stock and custom methods you support. The ATL Object Wiz­ ard doesn’t currently support stock methods, so you’ll add them to your class as if they were custom methods, which, in fact, they are except that you don’t get to choose the function signatures. You can use the Visual C+ + IDE to add the properties and methods to an in­ terface or you can make the source code changes manually. To use the IDE, right- click on the interface name in the ClassView and select Add Method or Add Property. After you respond to the resulting dialogs, the IDE changes two parts of your project. First, it updates the IDL for the specified interface and adds the appropriate property or method definition. Second, it updates your control class’s declaration (.h file) and adds a skeletal function body for each property accessor function or method. I prefer my methods to reside in the implementation (.cpp) file, so I fre­ quently convert the skeletal function body into a function prototype and place the function body in the class’s implementation file. The BullsEye control supports the stock methods and custom properties listed in Table 10.2 and Table 10.3, respectively. Listing 10.1 contains the complete defini­ tion for the IB ul 1 sEye interface, but here's an excerpt showing the definition of the Cente「Color custom property and the AboutBox stock method. [propput, bindable, requestedit, id(DISPID_CENTERCOLOR)] HRESULT CenterColor([in] OLE—COLOR newVal); [propget, bindable, requestedit, id(DISPID 一 CENTERCOLOR)] HRESULT CenterColor([out, retval] OLE_COLOR ^pVal); [i d(DISPID_ABOUTBOX)] HRESULT AboutBox(); 502 ATL INTERNALS Implementing Custom Properties and Stock and Custom Methods You’ll need to add a function prototype to your control class for each method added to the IDL in the previous step. When you use the IDE, it adds a skeletal function body for each method. For the above custom property and stock method, I added the following function prototypes to the CBullsEye class. STDMETHODIMP get_CenterColor(/*[out, retval]*/ 0LE_C0L0R *pVal); STDMETHODIMP put_CenterColor(/*[in]*/ OLE_COLOR newVal); STDMETHODIMP AboutBoxC); Declaring the Function Prototypes Note that you must declare each interface member function as using the STD­ METHODIMP calling convention. A system header file defines this macro to be the ap­ propriate calling convention for COM interface methods on a given operating system. This calling convention does change among various operating systems. By using the macro, rather than explicitly writing its expansion, your code is more portable across operating systems. On Win32 operating systems, the macro ex­ pands to HRESULT _ s td c a l 1. The code generated by the ATL wizards incorrectly uses the STDMETHOD macro. On Win32 operating systems, this macro expands as v irtu a l HRESULT — std ­ cal 1, which just happens to work. It won’t necessarily work on other operating sys­ tems that support COM. Basically, STDMETHOD should only be used in the original definition of an in­ terface; for example, this is typically the MIDL-generated header file. (MIDL,how­ ever, doesn’t use the macro but simply generates its expansion instead.) When implementing an interface method in a class (which we are doing in CBul 1 sEye), you should use the STDMETHODIMP macro. There are a couple of changes you should manually make to the method defini­ tions. First,all stock properties should have the bi ndabl e and requestedi t at­ tributes. This is because the stock property put methods fire change notifications to a container before and after changing a property. Therefore, you need to change each method as shown in the following section. Defining the Functions The function implementations are all pretty straightforward. The get_Center- Color method validates its argument and returns the value of the CenterColor property. STDMETHODIMP CBullsEye::get_CenterColor(0LE_C0L0R ^pVal) { if (NULL == pVal) return E_P0INTER; ACTIVEX CONTROLS 503 *pVal = m_clrCenterColor; return S_0K; } The p u t 一 CenterColor method, like all property change functions, is a bit more complicated. STDMETHODIMP CBul1sEye::put_CenterCo1or(0LE_C0L0R newVal) { if (m_clrCenterColor == newVal) return S_0K; if (!m 一 nFreezeEvents) if (FireOnRequestEdit(DISPIO_CENTERCOLOR) == S 一 FALSE) return S«FALSE; m_c1rCenterColor = newVal; II Save new color ::DeleteObject (m_centerBrush); // Clear old brush color icenterBrush = NULL; m_bRequiresSave = TRUE; // Set dirty flag if (!m_rFreezeEvents) FireOnChanged(DISPID_CENTERCOLOR); II Notify container of change Fi reViewChangeO; II Request redraw SendOnDataChange(NULL); II Notify advise sinks of change return S_0K; } First, the method checks to see if the new value is the same as the current value of the CenterColor property. If so, the value isn’t changing, so we exit quickly. Then, unlike the current stock property code, it properly checks to see if the con­ tainer presently doesn’t want to receive events, that is, if the freeze events count is nonzero. When the container has not frozen events, the put^Cente rCol or method fires the OnRequestEdi t event to ask the container for permission to change the CenterColor property. When the container refuses the change, put^Center- C o lo r returns S一 FALSE. When the container grants permission, put一CenterColor updates the mem­ ber variable in the control that contains the color. It also changes some values that cause the control’s rendering code to use the new color the next time the control redraws. ATL INTERNALS After the method changes the property, it sets the contrors dirty flag (m_bRe- qui resSave) to remember that the state of the control now needs to be saved. The various persistence implementations check this flag when executing their IsD i rty method. Next, the method fires the OnChanged event to notify the container of the prop­ erty change, assuming events are not frozen, of course. The CenterColor property is a property that affects the visual rendering (view) of the control. When a control changes such properties, the control should notify its container that the contrors appearance has changed by calling the Fi re- Vi ewChange function. In response, eventually, the container will ask the control to redraw itself. After that, the method notifies all advise sinks (which typically means the container) that the state (data) of the control has changed by calling SendOn- DataChange. Note that the state of a control changes independently of the contrors view. Some control property changes, like chaixges to CBul ^ sEye’s Beep property, have no effect on the appearance of the control. So the put-Beep method doesn’t call FireViewChange. The stock A b o u t B o x method simply displays the Help About (credits) dialog: STDMETHODIMP CBullsEye::AboutBoxO { CAboutDlg dig; dig.DoModal(); return S_0K; } Stock and Custom Events Adding Stock and Custom Events to the IDL Your IDL file describing the contrors default source interface must contain an en­ try for each stock and custom event method you support. As described previously, for maximum compatibility with all control containers, you should implement the default source interface as a dispatch interface. There is no current support in the IDE for adding event methods toa di spi n te 「face. The BullsEye control needs to support the two custom events described in Table 10.4. Here’s the updated IDL describing the event dispatch interface. All dis­ patch interfaces must be defined within the library block of an IDL file. [ uuid(19FF9872-36ED-lld2-ACO5-O0AOC9C8E50D), helpstring("Event interface for BullsEye Control") ACTIVEX CONTROLS 505 di spi nterface _IBullsEyeEvents { properties: methods: [id(DISPID^ONRINGHIT)] void OnRingHit(short ringNumber); [id(DISPID_ONSCORECHANGED)] void OnScoreChanged(1ong ringValue); }; You’ll also want to ensure that the IDL correctly describes the BullsEye class itself. The Bui 1 sEye cocl ass definition in the library block of the IDL file should define the IBullsEye interface as the default interface for the control and the _ IB u l1 sEyeEvents dispatch interface as the default source interface. [ uuid(7DC59CC5-36CO-llD2-ACO5-00A0C9C8E5OD), helpstring("BullsEye Class") 3 coclass BullsEye { [default] interface IBullsEye; [default, source] dispinterface _IBul1sEyeEvents; Adding Connection Point Support for the Events Many containers use the connection points protocol to hand the container’s sink in­ terface pointer to the event source (the control). Chs^pter 8 discusses connection points in detail, so I’ll just summarize the steps needed for a control. To support connection point events, a control must implement the IConnec­ t i onPoi ntContai ner interface as well as one IConnectionPoint interface for each outgoing (source) interface. Typically, most controls will support two source interfaces: the control’s default source dispatch interface (一IBu” sEyeEvents for the BullsEye control) and the property change notification source interface (IPropertyNoti fySi nk). Implementing the IConnectionPointContainer Interface When you initially create the source code for a control and select the Support Connection Points option, the ATL Object Wizard adds the IConnecti onPoi n t­ Contai nerlm pl base class to your control class declaration. This is ATL’s imple­ mentation of the IConnecti onPoi ntContai ner interface. You’ll need to add this 506 ATL INTERNALS base class explicitly if you decide to support connection points after creating the initial source code. class ATL_NO_VTABLE CBullsEye : 參 •攀 // Connection point container support publIc IConnectionPointContainerlmpl, • • • You’ll also need one connection point for each source interface supported by your control. ATL provides the IConnecti onPoi ntlm pl class, which is described in depth in Chapter 8, as an implementation of the IConnecti onPoi nt interface. Typically, you will not directly use this class, but will instead derive a new class from IConnecti onPoi ntlm pl, customizing the class by adding various event firing methods. Youf control will inherit from this derived class. Supporting Property Change Notifications ATL provides a specialization of IConnecti onPoi ntlm pl, called IProperty­ Noti fySi nkCP, that implements a connection point for the IPropertyNoti fy- Sink interface. The IPropertyN oti fySi nkCP class also defines the type defi­ nition _ATL_PR0P_N0TI FY_EVENT_CLASS (note the single leading underscore), as an alias for the CFi reP ropN oti fy E vent class. template public: typedef CFi r«PropNoti fyEvcnt When you use the ATL Object Wizard to create a full control that supports con­ nection points, the wizard adds the IPropertyN oti fySi nkCP base class to your control. You’ll have to add it otherwise. class ATL_NO_VTABLE CBullsEye : public IPropertyNotifySinkCP ACTI\/FX CONTROLS 507 Recall that a contrors propeny put methods, for both custom and stock prop­ erties, call the Fi reOnR°auestEdi t and Fi reOnChanged functions to send prop­ erty change notifications. These methods are defined in the CComControl Base class like this: template > class ATINO—VTABLE CComControl : public CComControl Base, public WinBase { public: HRESULT FireOnRequestEdit(DISPIO dispID) { T* pT * static 一 cast(this); return T: i^ATL.PROP.NOTIFY^EVEMT^.CLASS::FireOnRequestEdit CpT->CetUnknown()• dispID); } HRESULT Fi re0nChan9ed(DISPID dispIO) { T* pT * static_cast(this); return T : :一ATLJWPJilOTIFY一EVENT-CLASS:: Fi reOnChanged CpT->CetUnknoi*nOt dispIO); > Therefore, the call to Fi reOnChanged in a property put method of a CComControl- derived class actually is a call to the Fi reOnChanged of the class _ ATL__PR0P _NOTIFY_EVENT_CLASS (note the double leading underscore).within your actual control class. When you derive your control class from IP ro p e rty N o ti fy S i nkCP, your control class inherits a typedef for _ATI___PROP—NOTIFY一 EVENT一 CLASS (note the single leading underscore). typedef CFi rePro|>Not1fyEvent _ATt_PR0P_H0TI FY^EVENT^CLASS; For some odd reason (unknown to me), it’s the property map in your control class that equates the two types. The BEGIN_PROP一 MAP macro defines the type _ ATL •PROP—NOTIFY一EVENT一 CLASS (note the double leading underscore) as equiva­ lent to the type _ATL一PROP一NOTIFY一EVENT一 CLASS (note the single leading underscore). 508 ATL INTERNALS 参define BECIN^PROPJ^AP(theClass) \ typ«d«f J\TI_PROPJWnFY«EVENT^CLASS \ ^TU.PROPJWTIFY^EVENT.CLASS ; \ • • • In the BullsEye control, this means that when your property put method calls FireOnChanged, ■ This is actually a call to your CComControl: :F i reOnChanged base class method. ■ Fi reOnChanged calls CBullsEye: : jT L 一 PROP一 NOTIFY一 EVENT—CLASS:: FireOnChanged. ■ The property map aliases _ATL_PR0P 一 NOTIFY—EVENT一 CLASS to _ATL _PROP_NOTIFY_EVENT_CLASS. _ IP ro p e rty N o tify S i nkCP aliases _ATL_PROP_NOTIFY一 SINICCLASS to CFirePropNotifyEvent. • Therefore, you actually call the CBul 1 sEye:: CFi rePropNoti fyE vent:: Fi reOnChanged ftinction. The CFi rePropNoti fyEvent class contains two static methods, Fi reOn- RequestEdi t and Fi reOnChanged, that use your control’s own connection point support to enumerate through all connections for the IPropertyNoti fySi nk interface and call the OnRequestEdi t and OnChanged methods, respectively, of each connection. class CF1rePropNoti fyEvent public: static NRESULT F1r€0nRequestEd1tCIUnknoMn« pUnk, DISPID dispIO) { CCo«QIPtr pCPC(pUnk); if CfpCPC) return S.OK; CComPtr pCP; pCPC->F1ndConnec ti onPoi nt CIIOJPropertyNoti f ySi nk , &(>CP); p if (fpCP) return S_0K; CComPtr pEnutn; 1f (FAIL£0(pCP->£numConnectionsC&pEnum))) return S一OK; CONNECTOATA cd ; ACTIVEX CONTROLS while CpEnum->Next(l, &cd, NULL) S—OK) { if (cd.pUnk) { HRESULT hr * SJ)K; CComQIPtr pSink(cd.pUnk); if (pSink) hr » pS1nk->OnRequRelease(); if (hr m m S一 FALSE) return S_FALSE; } } return S_OK; } static HRESULT Fir^OnChangedCIUnkiKmn* pUnk, DISPID dispID) { // Code similar to above deleted for clarity • • • . while (pEnum->Next(l, &cd, NULL) S_OK) { if (cd.pUnk) { CCoraQIPtr pSink(cd.pUnk); If (pSink) pS1nk->OnChanged(dispID>; cd.pUnk->Release(); } All this means that you must derive your control class from IPropertyN oti fy_ SinkCP in order to get the typedef that maps the Fi reOnRequestEdit and Fi reOnChanged methods in CComControl to the actual firing functions in CFi re­ PropNoti fyEvent. When you don’t derive from IPropertyN oti fySi nkCP, you can still call the Fi reOnRequestEdi t and Fi reOnChanged*methods in CComControl. As long as your control class contains a property map, the code compiles without error and the method calls do nothing at runtime. ATL defines a ty p edef for the symbol _ATL_PROP_NOTI FY_EVENT_CLASS at global scope: typedef CFakeFi rePropNoti fyEvent ^TL„PROP_NOTIPir_EVENT_CLASS; 510 ATL INTERNALS When your control derives from IPropertyN oti fySi nkCP, you inherit a defini­ tion for ^ATL_PROP_NOTIFY_EVENT_CLASS that hides the global definition. When you don’t derive from IPropertyN oti fySi nkCP, the compiler uses the global definition just given. The CFakeFi rePropNoti fyEvent class looks like this: class CFakeFi rePropNoti fyEvent { public: static HRESULT Fi reOnRequestEdi t (IUnknown* " p U n k V , DISPIO 产 di s p I D V ) { return S_0K; } static HRESULT Fi reOnChanged (IUnknown* /*ptlnkV» DISPID /☆ dispIDV) { return S_0K; > J » I In the BullsEye control, this means that when you don’t derive from IP roper­ tyN oti fySi nkCP and your property put method calls Fi reOnChanged, _ This is actually a call to your CComControl:: Fi reOnChanged base class method. ■ Fi reOnChanged calls CBullsEye: •• 一 ATL_PR0P—N0TIFY一EVENT—CLASS:: FireOnChanged. ■ The property map aliases _^ATL_PR0P 一 NOTIFY一 EVENT—CLASS to _ATL _PR0P_N0TIFY_EVENT_CLASS. ■ The global typedef aliases _ATL«PR0P 一 NOTIFY-SINICCLASS to CFake­ Fi rePropNoti fyEvent. ■ Therefore, you actually call the CBullsEye: :CFakeFi rePropNoti fyEvent : : Fi reOnChanged function, which simply returns S一 OK. Supporting the Control's Event Connection Point You’ll want to use a specialization of IConnecti onPoi ntlm pl for each of your contrors event interfaces. Typically, a control implements only one event interface because Visual Basic and scripting languages can only hook up to the default event interface. This is the interface you describe in your object’s cocl ass definition with the [defaul t , sou rce] attributes. However, a custom C+ + client to your control can connect to any of its source interfaces. The specialized class derives from IConnecti onPoi ntlm pl and adds the ap­ propriate event firing helper methods for your events. Chapter 8 describes the de­ ACTIVEX CONTROLS tails of creating such a specialized class. The easiest way to create a specialized con­ nection point class is to right-click on the B u llsE y e class in the ClassView and select the Implement Connection Point item. Here’s the specialized connection point class, CProxy_IBul 1 sEye­ Events, generated by the wizard for the _IB ul 1 sEyeEvents dispatch interface. template class CProxy_IBullsEyeEvents : public IConnecti onPoi ntlnpl { // Warning: this class may be recreated by the wizard, public: VOID Fire_OnRingHit(SHORT ringNumber) { T* pT = static_cast(this); int nConnectionlndex; CComVariant* pvars * new CComVariant[1]; int nConnections = m_vec.CetSi2e(); for (nConnectionlndex = 0; nConnectionlndex < nConnections; nConnectionIndex++) { pT->Lock(); CComPtr sp = m_vec.GetAt(nConnectionlndex); pT->Unlock(); IDispatch* pDispatch = reinterpret—castInvoke(Oxl, IID一 MULL, LOCALE—USER_DEFAULT, DISPATCH_METHOD, &disp, NULL, NULL, NULL) } } delete[] pvars; } VOID Fi re_OnScoreChanged(LONC ringValue) { H Code similar to above deleted for clarity 512 ATL INTERNALS You use this class by adding it to the base class list of the control. Therefore, Bulls­ Eye now has two connection points in its base class list: class ATL—N0_VTABLE CBullsEye : // events and property change notifications public CProxy_IBu11sEyeEvents, public IProp€rtyNotifySinkCP, • • t Updating the Connection Map Finally, the IConnecti onPoi ntContai nerlmpl class needs a table that asso­ ciates source interface IIDs with the base class IConnecti onPoi ntlm pl special­ ization that implements the connection point. You define this table in your control class using the BEGIN 一 CONNECTION 一 POINT—MAP,CONN ECTION 一 POINT_ENTRY, and END_CONNECTION_POINT_MAP macros, as described in Chapter 8. Here’s the table for the CBul 1 sEye class: BECIN_CONNECTION_POINT_MAP(CBul^sEye) CONNECTION 一 POINT_ENTRY(DIID— IBul 1 sEyeEvents) CONNECnON.POINT.ENTRYdlD^IPropertyNotifySi nk) END_CONNECTION_POINT_MAP() Supporting IProvideClasslnfo2 Many containers, such as Visual Basic and Internet Explorer, use a control’s IProvideC1assInfo2 interface to determine the control’s event interface. When a control doesn’t support IP rovi deCl a s s ln f o2, these containers assume that the control doesn’t source events and they never establish a connection point to your control. Other containers, such as Test Container, don’t use a control’s IP rovi de­ Cl assInfo2 interface and browse a control’s type information to determine the default source interface. ATL provides an implementation of this interface in IProvideClassInfo- 2 Im p l. To use it, derive your control class from IP r o v i deCl a s s In fo 2 Im p l. The IProvi deCl assln f o2 interface itself derives from the IProvi deCl asslnfo in­ terface, so when you update your control’s interface map, you’ll need to provide entries for both interfaces. class ATL_NO_VTABLE CBullsEye : public IProvideClassInfo2Impl<&CLSID_Bul1sEye, &OIID— IBullsEyeEvents, ALIBID^TLInternal sLi b> , ACTIVEX CONTROLS 5 1 3 BEGIN _XOM_MAP(CBul1sEye) • •參 // Support for connection pcints COM_INTERFACE_ENTRY(IConnecti onPointContainer) C0M_INTERFACE_ENTRY(IProvideClassInfo2) COM一 INTERFACE_ENTRY(IProvi deClasslnfo) END_CONLMAP() On-Demand Rendering of Your Control’s View A control must be able to render its image when requested by its container. There are basically three different situations in which a control receives a rendering request: 1. The control has a window and that window receives a WM—PAIN下 message. A control handles this request in CComControl Base: :OnPai nt. 2. The control is windowless and the container’s window receives a WM_PAINT message encompassing the area occupied by the control. A control handtes this request in CComControl Base:: IVi ewObject_Draw. 3. The container requests the control to render its image into a metafile. A control handles this request in CComCont rol Base:: IDataOb j ect_GetData. Although all three types of rendering requests arrive at the control via different mechanisms, the ATL control implementation classes eventually forward the re­ quests to a contrors OnDrawAdvanced method. vi rtual HRESULT OnDrawAdvancedC ATLJ)RAWINFO& di ); ATL bundles all parameters to the rendering requests into an ATIORAWINFO structure. You need to use the information in this structure when drawing your con­ trol. Unfortunately, the structure definition itself is presently all the documentation that is available about the structure. However, most of the fields are simply copies of similar parameters to the IV i ewObj e c t:: Draw method Here’s the structure: struct ATLJ)RAWINFO UINT cbSize ; DWORD dwOrawAspect; // Set to sizeof (struct ATL.ORAWINFO) // Drawing aspect - typically // DVASPECT.COMTENT // CofBmonly -1, which specifies all of the // data LONG 1index 514 ATL INTERNALS OVTARCETDEVICE* ptd; // Render the object for this target device HDC hicTargetDev; II Information context for the target device HOC hdcOraw; U Draw on this device context LPCRECTt prc8ounds; // Draw within this rectangle LPCRECTL prcWBounds; II Window extent and origin when rendering a U metafile BOOL bOptimize; U Can control use drawing optimizations? BOOL bZoomed; II Object extent differs from drawing // rectangle? BOOL bRectlnHimetric; // Rectangle in HiMetric? SIZEL 2oomNum; // Rectangle size: X zoom * 2oomNum.cx/ " ZoomDen.cx SI2EL ZoomDen; // Extent size: Y zoom « ZoomNum.cy/ > ; // ZoomOen.cy ATL provides the following default implementation of the OnDrawAdvanced method in CComControl. Inline HRESULT CCo«Contro1Base: :OnOrawAdvanced(ATLJ)RAin:NRMt di) { BOOL bDeleteOC • FALSE; if (di.hicTargetOev NULL>;^t^ d1.hicTargetOcv • AtlCreateTargetOC(di.hdcOraw, di.ptd); bDeleteOC * (di.hicTargetOev !* di.hdcOraw); ::冷 RECTL rectBoundsOP ■ #di.prcBounds; BOOL bMetafile * CetOeviceCapsCdi.hdcOraw, TECHNOLOGY) •• OT^ETAFILE ; 1f (!bMetafile) { ::LPtoDP(di.hicTargetOev, CtPP0INT)4rectBounds0P, 2); SaveOCCdi.hdcOraw); SetMapModeCdl.hdcOraw. MM_TEXT); SetWlndowOrgExCdi.hdcDraw, 0, 0, NULL); S«tViewportOrgExCdi.hdcDraw, 0, 0, NULL); di.bOptimize - TRUE; //since we save the OC we can do this di•prcBounds - &rectBoundsDP|v GetZoomlnfoCdi) ACTIVEX CONTROLS 515 HRESULT hRes « OnOraw(dl); if (bDeleteDC) ::DeleteDC(d if (IbMetafile) RestoreOC(di return hRes; HRESULT hRes if (bDeleteDC) DeleteDCCdi.hicTargetDev) storeDC(di.hdcOraw, -1); CComCont r o l:: OnDrawAdvanced prepares a normalized device context for draw­ ing, then calls your control class’s OnDraw method. A normalized device context is called that because the device context has (some of) the normal defaults for a de­ vice context一 specifically, the mapping mode is MM一 TEXT, the window origin is 0,0, and the viewport origin is 0,0. Override the OnDrawAdvanced method when you want to use the device context passed by the container as is, without normalizing it. For example, if you don’t want these default values, you should override On- Drai vanced, rather than OnDraw, for greater efficiency. sn a container asks a control to draw into a device context, the container specifies whether or not the control can use optimized drawing techniques. When the bOptim ize flag in the ATLJRAWINFO structure is TRUE, the control can use drawing optimizations. This allows the control to avoid restoring certain settings of the device context after changing the setting. ■ When IDataObject_CetData calls OnDrawAdvanced to retrieve a rendering of the control in a metafile, IDataObj ect 一 GetDat a saves the device context state, calls OnDrawAdvanced, then restores the device context state. There­ fore, IDataObj ect_GetData sets the bOpti mi ze flag to TRUE. ■ When OnPai n t calls OnDrawAdvanced to have the control render to its win­ dow, the bOpti mi ze flag is set to FALSE. ■ When IV i ewObject_Draw calls OnDrawAdvanced to have the control render to the container’s window, the bO pti mi ze flag is set to TRUE if and only if the container supports optimized drawing. Therefore, when you override OnDrawAdvanced, you should always check the value of the bOptimize flag and restore the state of the device context as necessary. For a nonmetafile device context device, OnDrawAdvanced saves the state of the entire device context and restores it after calling your contrors OnDraw method. Because of this, the default OnDrawAdvanced method sets the bOptim ize flag Therefore, when you override OnDraw in ATL’s current implementation, the bO pti mi ze flag is always TRUE. This doesn’t mean you shouldn't check the flag. It means that you should always go to the effort o f supporting optimized drawing when overriding OnDraw because such support will always be used. to TRUE 516 ATL INTERNALS Listing 10.8 gives the drawing code for the BullsEye control. There are a few features of note in this code: ■ BullsEye supports transparent drawing via the BackStyl e stock property. When BackStyl e is 1 (opaque), the control uses the background color to fill the area around the bulls-eye. When BackStyl e is 0 (transparent), the control doesn’t draw to the area outside the circle of the bulls-eye. This leaves the area around the circle transparent, and the underlying window contents will show through. ■ BullsEye draws dillerently into a metafile device context versus another device context. There are some operations you cannot do when drawing to a metafile. Therefore, BullsEye sets up the device context slightly differently in these two cases. ■ BullsEye supports optimized drawing. Listing 10.8. BullsEye OnDraw and DrawBul 1 sEye methods HRESULT CBullsEye::0n0raw(ATL_DRAWINF0& di) { CRect rc - *(RECT*)di.prcBounds; HDC hdc = di.hdcDraw; // Create the background color brush only when necessary if (NULL == m_backBrush) { 0LE_C0L0R ocBack; HRESULT hr = get:一 BackColor (&ocBack); // Get the background // color ATLASSERT (SUCCEEDED (hr)); COLORREF crBack; // Translate the color to a C0L0RREF hr = ::01eTrans1ateColor (ocBack, NULL, &crBack); ATLASSERT (SUCCEEDED (hr)); m_backBrush = ::CreateSo1idBrush (crBack); // Create the II background brush ATLASSERT (NULL != m.backBrush); } H First, fill in background color in invalid area when BackStyle is Opaque ACTIVEX CONTROLS 517 if (1 /* Opaque*/ =* nunBackStyle) { int s = ::FillRect (hdc, Arc, m_backBrush); ATLASSERT (0 != s); int nPrevMapMode; POINT ptWOOrig, ptVOOrig; SIZE szWEOrig, szVEOrig; BOOL bMetafile « CetOeviceCapsCdi.hdcDraw, TECHNOLOGY)== DT_METAFILE; if (!bMetafile) { // OnOrawAdvanced normalized the device context. II We are now using MM_TEXT and the coordinates are in device // coordinates. II Establish convenient coordinate system nPrevMapMode = ::SetMapMode (hdc, MM_IS0TR0PIC); ATLASSERT (0 !■ nPrevMapMode); II Map logical 0,0 to physical center of rectangle BOOL bSuccess » ::SetWindowOrgEx (hdc, 0, 0, &ptW00rig); ATLASSERT (0 != bSuccess); bSuccess ■ ::SetViewportOrgEx (hdc, rc.left + rc.Width 0/2, rc.top -f rc.Height () / 2, &ptV00rig); ATLASSERT (0 !■ bSuccess); II Map logical extent (LOCWIDTH, LOCWIDTH) to physical extent of // rectangle bSuccess ■ ::SetWindowExtEx (hdc, LOGWIDTH, LOCWIDTH, AszWEOrig); ATLASSERT (0 !« bSuccess); bSuccess * ::SetViewportExtEx (hdc, rc.Width (), -rc.Height (), AszVEOrig); ATLASSERT (0 != bSuccess); } else { II We will be played back in ANISOTROPIC mapping mode II The rectangle will be in device units CRect rcBoundsDP (rc); 518 AT L INTERNALS // We can't use SetViewportOrg and SetViewportExt in a metafile // because the container will want to place the metafile. // // Find the center coordinate and the shorter side CSize size = rcBoundsDP.Si ze (); int iShortSide = min (size.cx, size.cy); CPoint ptCenter (rcBoundsDP.left + size.cx / 1 、 rcBoundsDP.top + size.cy / 2 ) ; // Compute the ratio of LOGWIDTH / shorter side double dRatio = (double) LOGWIDTH / (double) iShortSide ; // Set the logical origin of the wi ndow and swap coordinate axes BOOL bSuccess = SetWindowOrgEx (hdc, -int CptCenter.x u dRatio), int (ptCenter.y * dRatio), &ptW00rig); ATLASSERT (0 != bSuccess); // Set the logical extent of the wi ndow // Compensate for the drawing code which draws from -LOCWIOTH to // +L0GWIDTH bSuccess - SetWindowExtEx (hdc, int (size.cx * dRatio), -int (size.cy * dRatio), AszWEOrig); ATLASSERT (0 != bSuccess); XJ // Draw the BullsEye OrawBullsEye (di); II Note on optimized drawing: // Even when using optimized drawing, a control cannot II leave a changed mapping mode, coordinate transformation value, II selected bitmap, clip region, or metafile in the device context if (IbMetafile) { ::SetMapMode (hdc, nPrevMapMode); ::SetViewportOrgEx (hdc, ptVOOrig.x, ptVOOrig.y, NULL); ::SetViewportExtEx (hdc, szVEOrig.cx, szVEOrig.cy, NULL); ACTIVEX CONTROLS 519 ::SetWindowOrgEx (hdc, ptWOOrig.x, ptWOOrig.y, NULL); ::SetWindowExtEx (hdc, szWEOrig.cx, szWEOrig.cy, NULL); return S_OK; void CBul1sEye::DrawBul1sEye (ATL_DRAWINFO& di) { HDC hdc = di.hdcDraw; if Create the border pen only when necessary if (NULL == m.borderPen) { OLE—COLOR ocFore; HRESULT hr = get—ForeColor C&ocFore); ATLASSERT (SUCCEEDED (hr)); COLORREF crFore; hr = : .-OleTranslateColor (ocFore, NULL, AcrFore); ATLASSERT (SUCCEEDED (hr)); m_borderPen = ::CreatePen (PS—SOLID, 0, crFore); ATLASSERT (NULL != n_borderPen); // Create the center color brush only when necessary if (NULL =*= m_centerBrush) { COLORREF crCenter; HRESULT hr = ::01eTrans1ateColor (m_clrCenterColor, NULL, &crCenter); ATLASSERT (SUCCEEDED (hr)); m_centerBrush = ::CreateSolidBrush (crCenter); ATLASSERT (NULL 丨= m_cente 「B 「ush); // Create the alternate color brush only when necessary if (NULL -= nualternateBrush) { COLORREF crAlternate; HRESULT hr = ::01eTransiateColor (m_clrAlternateColor, NULL, &crAlternate); 520 ATL INTERNALS ATLASSERT (SUCCEEDED (hr)); m_alternateBrush = ::CreateSolidBrush (crAlternate); ATLASSERT (NULL !» m_alternateBrush); } // Compute the width of a ring short sRingCount; HRESULT hr = get—RingCount (AsRingCount); ATLASSERT (SUCCEEDED (hr)); int ringWidth = LOGWIDTH / (sRingCount * 2-1); // Draw the border between rings using the border pen HPEN hOldPen * (HPEN) SelectObject (hdc, m一 borderPen): HBRUSH hOldBrush = NULL ; // Draw each ring from outermost to innermost // This isn't nearly as efficient as it could be for (short i * sRingCount-1; i >= 0; i一 ) { // Compute the ring's bounding rectangle int ringDiameter = i * 2 ☆ ringWidth + ringWidth; int ringRadius * ringDiameter / 2; CRect rcRing (-ringRadius, ringRadius, ringRadius, -ringRadius); // Rings are numbered from 1 to N from center to outside. // However, the loop iterates from N-l to 0 from outside to // center. II Therefore, even-numbered rings should be the center color, II which implies odd-numbered rings use the alternate color. HBRUSHA ringBrush = i & 1 ? m_a1ternateBrush : m一 centerBrush; // Set the correct ring color HBRUSH hBrush = (HBRUSH) ::SelectObject (hdc, ringBrush); if (NULL == hOldBrush) // First time through, save the // original brush hOldBrush *= hBrush; ACTIVEX CONTROLS BOOL bStatus = // Draw the ring ::Ell ipse (hdc, rcRing.left, rcRing.right, rcRing.top, rcRing.bottom); ATLASSERT (0 != bStatus); } // When optimized drawing is not in effect, restore the original pen // and brush if (!di.bOptimize) { ::SelectObject (hdc, hOldPen); ::SelectObject (hdc, hOldBrush); } } Property Persistence A control typically needs to save its state upon request by its container. Various con­ tainers prefer different persistence techniques. For example, Internet Explorer and Visual Basic prefer to save a contrors state using a property bag, which is an asso­ ciation (or dictionary) of text name/VARIANT value pairs. The dialog editor in Vi­ sual C++ prefers to save a contrors state in binary form using a stream. Containers of embedded objects save the object(s) into a structured storage. ATL provides three persistence interface implementations: IPersi stStream lni tlm pl Saves properties in binary form into a stream. IPersi stStoragelmpl Saves properties in binary form in a structured storage. IP ersi stPropertyBaglmpl Saves properties as name/VARIANT value pairs. Most controls should derive from all three persistence implementation classes so that they support the widest variety of containers. The BullsEye control does this: class ATL_NO_VTABLE CBullsEye : II Persistence public IPersistStreamlnitlmpl, public IPersistStoragelmpl, public IPersi stPropertyBaglmpl, ATL INTERNALS As always, you need to add entries to the COM map for each supported interface. The persistence interfaces all derive from IP ersi st, so you need to add it to the COM map as well. BECIN_COM_MAP(CBulIsEye) • •參 // Persistence COM_INTERFACE_ENTRY(IPersistStreamlnit) C0M_INTERFACE_ENTRY2(IPersist, IPersistStreamlnit) COM_INTERFACE—ENTRY(IPersistStorage) COM_INTERFACE_ENTRY(IPersistPropertyBag) END_COM_MAP() All three persistence implementations save the properties listed in the control’s property map. You define the property map using the BEGIN一PROP一 MAP and END_PROP__MAP macros. Here’s the CBul 1 sEye class’s property map: BFGIN_PROP_MAP(CBul1sEye) PROPJ)ATA_ENTRY("_cxM, nusizeExtent.cx, VT_UI4) PROP—DATA»ENTRY("_cy"• m—sizeExtent.cy, VT.UI4) PROP_ENTRY(uBackStyle", DISPID.BACKSTYLE, CLSID.Bul1sEyePropPage) PROP—ENTRY("Beep", DISPID—BEEP, CLSIO_Bul1sEyePropPage) PROP—ENTRY("Enabled", OISPID.ENABLED, CLSIDJull sEyePropPage) PROP_ENTRY("RingCount", DISPID—RINCCOUNT, CLSID.Bul1sEyePropPage) PROP^ENTRYC'AlternateColor", OISPIDJ\LTERNATECOLOR, CLSID.StockColorPage) PROP_ENTRY("BackColor", DISPID.BACKCOLOR, CLSID_StockColorPage) PROP一 ENTRY("CenterColor", DISPID_CENTERCOLOR, CLSID.StockColorPage) PROP_ENTRY(MForeColorM, DISPID.FORECOLOR, CLSID^StockColorPage) END 一 PR0PJ4APO The ATL Object Wizard adds the first two PROP_DATA_ENTRY macros to a contrors property map when it generates the initial source code. These entries cause ATL to save and restore the extent of the control. When you describe a persistent property using a PROP_DATA_ENTRY macro, ATL directly accesses the member variable in the control. You must explicitly add entries for any additional properties that the control needs to persist. The BullsEye control lists all but one of its persistent properties using the PROP—ENTRY macro. This macro causes ATL to save and restore the specified property by accessing the property using the default dispatch interface for ACTIVEX CONTROLS 523 the control. Alternatively, you can use the PROP一 ENTRY—EX macro to specify the IID, other than IID —ID i spatch, of the dispatch interface that supports the prop­ erty. You’d use the PROP_ENTRY_EX macro when your control supports multiple dispatch interfaces with various properties accessible via different dispatch inter­ faces. Supporting multiple dispatch interfaces is, generally speaking, not a good thing to do. One caution: Don’t add a PROP_ENTRY macro that has a property name containing an embedded space character Some relatively popular containers, for example, Visual Basic, provide an implementation of IPropertyBag: :Wri te that cannot handle names with embedded spaces. This is a bug in the current version of Visual Basic, as other containers allow space-separated property names. For properties described with the PROP_ENTRV and PROP一ENTRY一 EX macros, the various persistence implementations query for the appropriate interface and cal】 ID ispatch:: Invoke, specifying the DISPID from the property m ap entry to get and put the property. There is presently a bug in the ATL implementation of property bag persistence for properties described using the PROP_DATA^ENTRY macro. The problem is in the Atl IPersi stPropertyBag 一 Load function. Chapter 6 describes the problem in detail and shows you how to fix it. The summary is: Have the BullsEye control pro­ vide an IP ersi stPropertyBag_Load method that simply calls a fixed version of AtlIPersistPropertyBag—Load. As it turns out, I need to provide an IPersi stPropertyBag_Load method anyway. The BullsEye control has one additional property— the Ri ngValues in­ dexed (array) property. The ATL property map doesn’t support indexed properties. In order to persist such properties, you must explicitly implement the IP ersi st- S tr e a m ln it 一 Save, IPersistStreamlni t 一 Load, IPersistPropertyBag_ Save, and IP ersi stPropertyBag—Load methods normally provided by the ATL persistence implementation classes and read/write the indexed property. Here's an example from the BullsEye control. It calls a fixed version of A tl IP ersi st- P ropertyB ag_L oad, then saves the indexed property. HRESULT CBul1sEye::IPersistPropertyBag_Load(LPPROPERTYBAG pPropBag, LPERRORLOC pErrorLog, ATL.PROPMAP_ENTRY* pMap) { if (NULL «= pPropBag) return E_POINTER; // Load the properties described in the PROP一MAP II Work around ATL 3.0 bug in AtlIPersistPropertyBag_Load HRESULT hr * FixedAtlIPersistPropertyBag_Load(pPropBag, pErrorLog, pMap, this, GetUnknownO); if (SUCCEEDED(hr)) m_bRequi resSave = FALSE; 524 ATL INTERNALS if (FAILED (hr)) return hr; // Load the indexed property-RingValues // Get the number of rings short sRingCount; get_RingCount (AsRingCount); II For each ring, read its value for (short nlndex » 1; nlndex <* sRingCount; nlndex++) { // Create the base property name CComBSTR bstrName ■ OLESTRC'RingValue "); // Create ring number as a string CComVariant vRingNumber » nlndex; hr = vRingNumber.ChangeType (VT.BSTR); ATLASSERT (SUCCEEDED (hr)); // Concatenate the two strings to form property name bstrName +* vRi ngNumber.bstrVal; II Read ring value from the property bag CComVari ant vValue - OL; hr = pPropBag->Read(bstrName f AvValue, pErrorLog); ATLASSERT (SUCCEEDED (hr)); ATLASSERT (VT__I4 vValue.vt); if (FAILED (hr)) { hr = U N E X P E C T E D ; break; } // Set the ring value put^RingValue (nlndex, vValue.lVal); } if (SUCCEEDED(hr)) m_bRequiresSave = FALSE; return hr; ACTIVEX CONTROLS 525 IQuickActivate Some control containers ask a control for its IQui ckActi vate interface and use the interface to exchange quickly a number of interfaces between the container and the control during the contrors activation process, thus the interface name. ATL provides an implementation of this interface, IQui ckActi vatelm pl, which, by default, full, composite and HTML controls use. However, a control con­ tainer also provides a control a few ambient properties during this quick activation process, which the ATL implementation doesn’t save. Should your control need these ambient properties— BackColor, ForeColor, and Appearance— it’s more efficient to save them during the quick activation process than to incur three more round-trips to the container to fetch them later. The tricky aspect is that a container might not quick activate your control. Therefore, the control should save the ambient properties when quick activated, or retrieve the ambient properties when the container provides the contrors client site, but not both. However, it’s easy to add this functionality to your control. When a container quick activates your control, it calls the control’s IQui ck­ Acti v ate : :Qui ckActi vate method, which is present in your contrors IQui ck­ A cti vatelm pl base class. This method delegates the call to your control class’s IQui ckActi vate_Qui ckActi vate method. By default, a control class doesn’t provide the method, so the call invokes a default implementation supplied by CComControl Base. You simply need to provide an implementation of the IQui ck­ Acti vate_Qui ckActi vate method that saves the ambient properties and for­ wards the call to the method in CComControl Base, like so: HRESULT CBul1sEye::IQuickActivate_QuickActivate(QACONTAINER *pQACont, QACONTROL *pQACtrl) { m_c1rForeColor = pQACont->colorFore; m_clrBackColor = pQACont->colorBack; n一 nAppearance = (short) pQACont->dwAppearanee; m_bAmbi entsFetched = true; HRESULT hr = CComCont rolBase::IQui ckActi vate_Qui ckActi vate(pQACont, p Q A C tr l); return hr; } Note that the function also sets a flag, m_bAmbi entsFetched, to remember that it has already obtained the ambient properties and therefore shouldn’t fetch them 526 ATL INTERNALS again when the control receives its client site. BullsEye initializes the flag to f al se in its constructor and checks the flag in its 101 eObject_SetCl i entSi te method like this: HRESULT CBulIsEye::101eObject^SetClientSite(101eClientSite *pClientSite) { HRESULT hr = CComControlBase::101eObject_SetCli entSi te(pCli entSi te); if (!m一 bAmbientsFetched) { HRESULT hr = CetAmbientBackColor (m_d rBackColor); hr = GetAmbientForeColor (m_clrForeColor); hr - GetAmbientAppearance (m_nAppearance); } return hr; Component Categories Frequently you’ll want your control to belong to one or more component categories. For example, the BullsEye control belongs to the “ATL Internals Sample Compo- nents” category. Additionally, BullsEye is a member of the “Safe for Initialization” and “Safe for Scripting” categories so that the control may be initialized and ac­ cessed by scripts on an HTML page without security warnings. Adding the proper entries to the control’s category map registers the class as a member of the specified component categories. BullsEye uses this category map: BECIN_CATECORY_MAP(CBul1sEye) XMPLEMENTED_CATECORY(CATID_ATLINTERNALS_SAMPLES) IMPLEMENTED_CATEGORY(CATID_SafeForScripting) IMPLEMENTED—CATECORY(CATID_SafeFo「Ini tializing) END_CATEGORV_MAP() Registering a control as a member of the Safe for Initialization and/or Safe for Scripting component categories is a static decision. In other words, you’re deciding that the control is or is not always safe. A control may prefer to restrict its func­ tionality at runtime when it needs to be safe for initialization or scripting but at other times have its full, potentially unsafe, functionality available. Such controls must implement the IO bjectS afety interface. ATL provides a default implementation of this interface in the 10bj ectSafetylmpl class. You specify, as a template parameter, the safety options supported by the control, and a ACTIVEX CONTROLS 527 container can use methods of this interface to selectively enable and disable each supported option. A control can determine its current safety level, and potentially disable or enable unsafe functionality, by checking the m 一 dwCu r re n t Sa fe ty mem­ ber variable. You use this implementation class like most of the others, derive your control class from the appropriate template class, and add the proper interface entry to the COM interface map. BullsEye does it like this: class ATL_NO_VTABLE CBullsEye : II Object safety support public IObjectSafetylmpIf BECIN_COM_MAP(CBul 1sEye) II Object safety support COM_INTERFACE_ENTRY(IObjectSafety) END 一 COM_MAP() ICategorizeProperties Visual Basic provides a property view that displays the properties of a control on a form. The property view can display the properties on a control alphabetically or grouped by arbitrary categories. Figure 10.3 shows the categorized list of the Bulls­ Eye control’s properties when the control is contained on a Visual Basic form. A control must implement the IC ategori zeProperti es interface so that Vi­ sual Basic can display the contrors properties in the 叩propriate categories in its property view. Unfortunately, this interface isn’t presently defined in any system IDL file or any system header file, and ATL provides no implementation class for the interface. So here's what you need to do to support it. First, here’s the IDL for the interface: [ object, local, uuid(4D07FC10-F9Bl-HCE-B001-00AA006884E5), helpstringC'ICategorizeProperties Interface"), pointer_defau1t(unique) ATL INTERNALS nterface ICategorizeProperties : IUnknown typedef [public] int PROPCAT; const int PROPCAT_Nil const int PROPCAT一Mi sc const int PROPCAT_Font const int PROPCAT.Position const i nt PROPCAT J\ppearanee const int PROPCAT_Behavior const int PROPCAT_Data const int PROPCAT_List const int PROPCAT_Text const int PROPCAT^Scale const int PROPCAT—DDE -5 =-11 HRESULT MapPropertyToCategory([in] DISPID dispid, [out] PROPCAT* ppropcat); HRESULT CetCategoryName([in] PROPCAT propcat, [in] LCID lcid, [out] BSTR* pbstrName); I keep this IDL in a separate file, C ate g o ri zePrope r t i e s . id l , and import the file into the B u ll sEye. i d i file. This way, when Microsoft finally adds the interface to a system IDL file, I can simply remove the import from the B u ll sEye. id l file. You implement the interface like all interfaces in ATL: Derive your control class from ICategori zeProperties, add the interface entry to the control’s interface map, aad implement the two methods, MapPropertyToCategory and CetCate­ goryName. Note that there are eleven predefined property categories with negative values. You can define your own custom categories, but be sure to assign them pos­ itive values. The MapPropertyToCategory method returns the appropriate property cate­ gory value for the specified property. STDMETHODIMP CBul1sEye::MapPropertyToCategory(/*[in]*/ DISPID dispid, /☆[out]*/ PROPCAT* ppropcat) { if (NULL == ppropcat) return E_POINTER; ACTIVEX CONTROLS 529 switch (chspid) { case DISPID一FORECOLOR: case DISPID—BACKCOLOR: case DISPID_CENTERCOLOR: case DISPID^ALTERNATECOLOR: case DISPID.RINCCOUNT: case OISPID.BACKSTYLE: *ppropcat = PROPCAT^Appearance; return S一 OK; case DISPID.BEEP: case DISPID_ENABLED: *ppropcat - PROPCAT.Behavi or; return S_OK; case DISPID 一 RINGVALUE: *ppropcat = PROPCAT_Scoring; return S 一 OK; default: return E一 FAIL; The CetCategoryName method simply returns a BSTR containing the category name. You only need to support your custom category values, because Visual Basic knows the names of the standard property category values. STDMETHODIMP CBul1sEye::CetCategoryName(/*[in]*/ PROPCAT propcat, /☆[in]*/ LCID lcid, /★[out]*/ BSTR* pbstrName) { 、 if(PROPCAT—Scoring == propcat) { *pbstrName = ::SysAllocString(L"ScoringM) ; return S_OK; > return E一 FAIL; } BullsEye supports one custom category, “Scoring,” and assori ites its Ring- Val ue property with the category. Unfortunately, the Ri ngVal ue pr )perty is an in­ dexed property and Visual Basic doesn’t presently support indexed properties. Thus, the Ri ngVal ue property doesn’t appear in Visual Basic's property view, ei­ ther in the alphabetic list or the categorized list. AT L INTERNALS Per-Property Browsing When Visual Basic and similar containers display a controls property in a property view, they can ask the control for a string that better describes the property’s cur­ rent value than the actual value of the property. For example, a particular property may have valid numerical values of 1,2, and 3, which represent the colors red, blue, and green, respectively. When Visual Basic asks the control for a display string for the property when the property has value 2, the control returns the string “blue.” A container uses the contrors IPerPropertyBrowsing interface to retrieve the display strings for a contrors properties. When the control doesn’t provide a dis­ play string for a property, some containers, such as Visual Basic, will provide de­ fault formatting if possible.1 Of course, the container can always simply display the actual property value. Notice in Figure 10.3 that the Visual Basic property view displays “Yes” for the value of the Beep property (which was set to - 1) and “Transparent” for the Back­ S ty l e property (which was set to 0). To provide custom display strings for a prop­ erty's value, your control must implement IPerPropertyBrowsing and override the GetDi splayString method. You return the appropriate string for the re­ quested property based on the property’s current value. Here’s the GetDi spi ay- S tri ng method for the CBul ^ sEye class: STDMETHODIMP CBullsEye::CetDisplayString(DISPID dispid,BSTR *pBstr) { ATLTRACE2CatlTraceControls,2,_T("CBul1sEye::GetDispiayString\n")); switch (dispid) { case DISPID_BEEP: if (VARIANT_TRUE -= m_beep) *pBstr ■ SysAllocString (OLESTR("Yesn)); else ★pBstr = SysAllocString C0LESTR("No*')); return *pBstr ? S_0K : E^OUTOFMEMORY; case DISPID.BACKSTYLE: if (1 == m_nBackStyle) *pBstr = SysAllocString (0LESTR("0paqueM)); 1 Visual Basic first queries a control for IPerPropertyBrowsing to retrieve the display strings for a property. When that query fails, Visual Basic will load the type library and retrieve the enumerated val­ ues that were specified in the IDL for the property, if available. Failing that, Visual Basic displays the ac­ tual property value. ACTIVEX CONTROLS 531 else *pBstr = SysAllocString (OLESTR("T「ansparent")); return *pBstr ? S_OK : E_OUTOFMEMORY; case DISPID_ALTERNATECOLOR:// Make Visual Basic apply default // formatting case DISPID一 BACKCOLOR:// for these color properties. case DISPID_CENTERCOLOR: II Otherwise it displays color values // in decimal case DISPID一FORECOLOR:II and doesn't draw the color sample // correctly. return S一FALSE; // This is an undocumented return // value that works... return IPerPropertyBrowsinglmpl:: GetDisplayString(dispid, pBstr); } The IPerPropertyBrowsi nglmpl ::CetDi spi ayStri ng implementation fetches the value of the specified property and, if it’s not already a BSTR, converts the value into a BSTR using Vari antChangeType. This produces relatively un­ interesting display strings for anything but simple numerical value properties. Visual Basic will provide default formatting for certain property types, such as OLE_COLOR and VARIANT一 BOOL properties, but only if your GetDi spi ayS tri ng method doesn’t provide a string for the property. The default implementation fails when the property doesn’t exist, or the property exists but cannot be converted into a BSTR, or the BSTR memory allocation fails. This basically means that the default implementation of GetDi spi ay S tri ng often provides less than useful strings for many properties. BullsEye’s GetDi spi ayS tri ng method lets Visual Basic provide default for­ matting for all its OLE_COLOR properties by returning S一 FALSE when asked for those properties. This value isn’t documented as a valid return value for Cet­ Di spi a y S tri ng, but there are a couple of convincing reasons to use it: The default ATL implementation of CetDi spi ay S tri ng returns this value when it cannot pro­ vide a display string for a property and . . . it works. When you let Visual Basic provide the display string for an OLE_COLOR prop­ erty, it displays the color value in hexadecimal and displays a color sample. ATL’s default implementation displays the color value in decimal, and the sample image is ATL INTERNALS typically always black. When you let Visual Basic provide the display string for a VARIANT.BOOL property, Visual Basic displays wTrue” and “False.” ATL’s default im­ plementation displays “一 1” and “0 ,” respectively. Also notice in Figure 10.3, that when you click on a property in Visual Basic’s property view in order to modify the property,a drop-down arrow appears to the right side of the property value. Clicking on this arrow produces a drop-down list containing strings representing the valid selections for the property. You provide this support via the IPerPropertyBrowsi ng interface too. A container will call the interface’s GetPredefi nedStri ngs method to retrieve the strings the con­ tainer displays in the drop-down list. For each string, the method also provides a DWORD value (cookie). When a user selects one of the strings from the drop-down list, the container calls the interface's GetPredef i nedVal ue method and provides the cookie. The method returns the property value associated with the selected string. The container then typically performs a property put ID i spatch call to change the property to the predefined value. The BullsEye control supports predefined strings and values for the Beep and BackStyl e properties, as shown in the following code. /* GetPredef1nedStri ngs */ fdefine DIM(a) (sizeof(a)/sizeof(a[0])) static const LPCOLESTR rszBeepStrings [] static const DWORD rdwBeepCookies [] static const VARIANT—BOOL rvbBeepValues [] { OLESTRC'Yes, make noise")f OLESTRC'No, be mute”)}; { 0, 1 }; { VARIANT-TRUE, VARIANT_FALSE }; static const UINT cBeepStrings ■ DIM(rszBeepStrings); static const UINT cBeepCookies * DIM(rdwBeepCookie s); static const UINT cBeepValues = DIH(rvbBeepValues); static const LPCOLESTR static const DWORD static const long rszBackStyleStrings [] * { 0LESTR("0paque"), OLESTRC'Transparent") }; rdwBackStyleCookies [] « { 0, 1 >; rvbBackStyleValues [] * { 1, 0 }; ACTIVEX CONTROLS 533 static const UINT static const UINT static const UINT STOMETHOOIMP CBul1sEye::CetPredefinedStrings(/*[in]*/ DISPID dispid, /*[out]*/ CALPOLESTR ♦pcaStringsOut, /•[out]*/ CAOWORD *pcaCookiesOut) { ATLTRACE2(atlTraceControls,2•—T("CBul1sEye::CetPredefinedStrings\nM)); if (NULL =* pcaStringsOut || NULL — pcaCookiesOut) return E_POINTER; ATLASSERT CcBeepStrings ■■ cBeepCookies); ATLASSERT (cBeepStrings cBeepValues); ATLASSERT (cBackStyleStrings ** cBackStyleCookies) ATLASSERT (cBackStyleStrings ■■ cBackStylcValues); pcaStringsOut->cElems « 0; pcaStringsOut->pElems » NULL; pcaCookiesOut->cElems * 0; pcaCookiesOut->pElems « NULL; HRESULT hr » S_OIC; switch (dispid) { case DISPID_BEEP: hr * SetStrings (cBeepValues, rszBeepStrings, pcaStringsOut); if (FAILED (hr)) return hr; return SetCookies (cBeepValues, rdwBeepCookies, pcaCookiesOut); case DISPID.BACKSTYLE: hr ■ SetStrings (cBackStyleValues, rszBackStyleStrings, pcaStringsOut) ; if (FAILED (hr)) return hr; return SetCookies (cBackStyleValu«s, rdwBackStyleCooki es, pcaCookiesOut); } return IPerPropertyBrowsi nglmpl:: CetPredefi nedStri ngs(di spi d, pcaStringsOut, pcaCookiesOut); 534 ATL INTERNALS /* CetPredefi nedValue */ STDMETHODIMP CBul1sEye::GetPredefinedValue(DISPID dispid, DWORD dwCookie, VARIANT* pVarOut) { if (NULL ** pVarOut) return E_POINTER; ULONC i ; switch (dispid) { case DISPID一 BEEP: // Walk through cookie array looking for matching value for (i = 0; i < cBeepCookies; i++) { if (rdwBeepCookies[i] == dwCookie) { pVarOut->vt = VT_BCX)L; pVar0ut->boo1Val ■ rvbBeepValues [i]; return S_0K; > } return E_INVALIOARC; case DISPID_BACKSTYLE: II Walk through cookie array looking for matching value for (i * 0; i < cBackStyleCooki es; i++) { if CrdwBackStyleCooki es[i] == dwCookie) { pVarOut->vt 篇 VT—I4; pVarOut->1Val = rvbBackStyleValues [i]; return S_0K; > } return EJtNVALIOARG; return IPerPropertyBrowsinglmpl::GetPredefi nedValue(dispid, dwCookie, pVarOut); Some containers will let you edit a contrors property using the appropriate property page for the property. When you click on such a property in \^sual Basic’s ACTIVEX CONTROLS 535 property view, Visual Basic displays a small pushbutton containing to the right of the property value. Clicking on this button displays the control's property page for the property. A container uses a control’s IPerPropertyBrowsi ng: :MapProperty- ToPage method to find the property page for a property. Unfortunately, ATL 3.0’s IPerPropertyBrowsi nglmpl class has a small bug in its implementation. When it finds a property listed in the property map, it returns the CLSID contained in the map. However, the property map serves two functions. It maps properties to prop­ erty pages and it also lists properties supported by the persistence interfaces. For a property that you want persisted but for which you have no property page editing support, you add an entry such as the following to the property map: PROP-ENTRY(”SomePrope「ty ”, DISPID—SOMEPROPERTY, CLSID_NULL) IPerPropertyBrowsi nglmpl finds this entry in the property map and returns CLSID__NULL for the property page class. This causes Visual Basic to display the pushbutton, and when you click on it, you receive an error message stating that CLSID_NULL is not registered. To correct the problem, override MapProperty- ToPage in your control. Invoke IPerPropertyBrowsi nglm pl: :MapProperty- ToPage and when it finds the requested property, check for CLSID一NULL as the output. In this case, return the proper error status: PER PROP—E—NOPAGEA VAIL ABLE. STDMETHODIMP CBul1sEye::MapPropertyToPage(DISPID dispid, CLSID *pClsid) { HRESULT hr * IPerPropertyBrowsinglmpl::MapPropertyToPage(dispid, pCl si d) ; if (SUCCEEDED(hr) && CLSID.NULL == *pClsid) hr =* PERPROP_E^NOPACEAVAILABLE; return hr; } Keyboard Handling for an ActiveX Control When an ATL-based ActiveX control has the focus on a Visual Basic form, it does not give the focus to the default button on the form (the button with a Defaul t property of T rue) when you press Enter. ATL provides implementations of the 101 eControl:: CetControl Info and the 101 elnPl aceActi veObject:: Transi ateAccel erator methods that return E_N0TIMPL. A container calls a control’s CetControl In fo method to get the control’s keyboard mnemonics and keyboard behavior, and it calls the control’s TranslateAccel erator method to process the key presses. 536 ATL INTERNALS BullsEye overrides the two default implementations provided by ATL with the following code: STOMETHOOIMP CBul1sEye::CetControlInfo(CONTROLXNFO *pci) { if(!pci) return E_POINTER; pci->hAccel = NULL; pci->cAccel = 0; pci->dwFlags = 0; return S一OK; } typedef enum tagKEYMODIFIERS { KEYMOD.SHIFT = 0x00000001, KEYMOD_CONTROL = 0x00000002, KEYMOO^ALT = 0x00000004 } KEYMOOIFIERS; STDMETHODIMP CBul1sEye::TransiateAccelerator(MSG *pMsg) { if (((pMsg->message >« WM_KEYFIRST) SA (pMsg->message <= WM 一 KEYLAST)) &A C(VK_TAB « pMsg->wParam) |丨 (VK_RETURN ** pMsg->wParam))) { CComQIPtr<101eControlSite, &IID_I01eControlSite> spCtrlSi te(m_spCli entSi te); if (spCtrlSite) { DWORD km - 0 ; km CetKeyState (VK_SHIFT) < 0 ? KEYMOD.SHIFT : 0; km |- CetKeyState (VK_CONTROL) < 0 ? KEYMOD_CONTROL : 0; km I- CetKeyState (VK_MENU) < 0 ? KEYMODJVLT : 0; return spCtrlSite->TranslateAccelerator (pMsg, km); return S^FALSE; } When the BullsEye control has the input focus, these method implementations pass all Tab and Enter key presses to the container for processing. This implementation one to tab into and out of the BullsEye control while the control has the ACTIVEX CONTROLS 537 input focus, pressing the Enter key activates the default pushbutton on a Visual Ba­ sic form, if any. Of course, for the BullsEye control it doesn’t make much sense to allow a user to tab into the control. You can use the Mi scS tatus for a control to inform the con­ trol's container that the control doesn’t wish to be activated by tabbing. A container asks a control for its Mi scStatus setting by calling the control’s 101 eO bject:: GetMi scStatus method. ATL provides an implementation of this method in the 101 e C o n tro l Imp! class. It looks like this: STOHETHOOCCetMi scStatus)(DWORD dwAspect, DWORD *pdwStatus) { ATLTRACE2(atlTraceControls,2,.T(HI01e0bjectImpl::CetMisc5tatus\nM)); return 01eRegGetMi scStatus(T::CetObjectCLSID(), dwAspect( pdwStatus); } This code simply delegates the call to the 01 eRegGetMi scS tatus function, which reads the value from the contrors registry entry. A control can have multiple Mi sc­ Status values— one for each drawing aspect supported by the control. Most con­ trols support drawing aspect DVASPECT一CONTENT, which has the value of 1. You specify the drawing aspect as a subkey of Mi scStatus. The value of the subkey is the string of decimal numbers comprising the sum of the desired OLEMISC enumer­ ation values. For example, BullsEye uses the following Mi scStatus settings: OLEMISCLSETCLIENTSITEFIRST 131072 0LEMISC_N0UIACTIVATE 16384 OLEMISCLACTIVATEWHENVISIBLE 256 OLEMISC^INSIDEOUT 128 CANTLINKINSIDE • 16 0LEMISC.REC0MP0SE0NRESIZE 1 The sum of these values is 147867, so you specify that as the value of the subkey called “1” of your class. ForceRemove {7DC59CC5-36C0-11D2-AC05-OOAOC9C8E50D} « s 'BullsEye Class' { 'MiscStatus' * s '0* { ATL INTERNALS •1, = s '147857' > } Alternatively, BullsEye could override the GetMi scStatus method and pro­ vide the desired value, and the registry entry would not be needed: STDMETHODIMP CBul1sEye::CetMi scStatus (DWORD dwAspect, DWORD *pdwStatus) { if (NULL *=* pdwStatus) return E_P0INTER; *pdwStatus = OLEMISC.SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_INSIDEOUT | OLEMISC_CANTLINKINSIDE I OLEMISC_RECOMPOSEONRESIZE 丨 OLEMISC_NOUIACTIVATE ; return S_OK; } The OLEMISC^NOUIACTIVATE setting is the one that prevents Visual Basic from giving the BullsEye control the input focus when a user attempts to tab into the control. Summary ActiveX controls use much of the functionality discussed so far. An ATL-based control typically supports properties and methods using ATL’s ID i spatchlmpl support. In addition, a control typically fires events; therefore, it often derives from IConnecti onPoi ntContai nerlmpl and uses a connection point proxy- generated class (IC o n n e c ti onPoi n tlm p l -derived) for each connection point. A control generally requires persistence support, so uses one or more of the persis­ tence implementation classes: IPersi stsStreamlni tlmpl, IPersi stProper­ tyBaglmpl, and IPersi stsStoragelmpl. In addition there are numerous other interfaces many controls should imple­ ment so they integrate well with various control containers, such as Visual Basic. In the next chapter, you’ll learn how ATL supports hosting ActiveX controls and how to write a control container using ATL. CHAPTER 11 Control Containment Containment of COM controls can take many forms. A window can contain any number of COM controls, as can a dialog or another control (called a composite con­ trol). All these containers share common characteristics, which is the subject of this chapter. How Controls Are Contained To contain a COM control,1 a container must do two things: 1. Provide a window to act as the parent for the child COM control. The parent window may be used by a single child COM control or shared between many. 2. Implement a set of COM interfaces that the control uses to communicate with the container. The container must provide at least one object per control, called the site, to implement these interfaces. However, the interfaces may be spread between up to two other container-provided objects, called the document and the frame. The window provided by the container may be a parent window of the control or, in the case of a windowless control, it may be shared by the control. The control will use the window in its interaction with the user. The interfaces implemented by the container are used for integration with the control and mirror those interfaces im­ plemented by the control. Figure 11.1 shows the major interfaces implemented by the container and how they are mirrored by those implemented by the control. As with Ch叩ter 10,full coverage of the interaction between controls and con­ tainers is beyond the scope of this book. Refer to the sources listed in Cluster 10 for more information.2 However, this chapter will present those things you need to know to host controls both in standalone applications and inside of COM servers. 'This chapter doesn’t distinguish between OLE controls and ActiveX controls. 2 You may also want to refer to the MSDN article, “N’otes on Implementing an OLE Control Container,- for control container-specific information. 539 540 ATL INTERNALS Container Object IntarfacM Frame Document -O lOlelnPlacel Window ■O lOielnPlaceUI- Window IDispatch ^ (ambient properties) O lAdviseSink ------------ O lOleCJientSito ---------- ------O lOloControlSite -------- Control Interfaces OelnPlaceActive-Object Q - _______ __ IViewObject Q * Control Figure 11.1. Container and control interfaces Your hosting options include windows, dialogs, and composite controls. Before div­ ing into the details of dialogs or controls hosting other controls, let's start with the basics by examining control containment in a simple frame window. Basic Control Containment Control Creation The control creation process in ATL exposes the core of how ATL hosts controls. Figure 11.2 shows the overall process, and what follows is a detailed look at the relevant bits erf code involved. CAxHostWindow ATL’s implementation of the required container interfaces is called CAxHost­ Window:3 // This class is not cocreateable class ATLJW.VTABLE CAxHostWindow : public CCoiBCoC1ass, public CComObjectRootEx• 3Thls class is defined in atl host. h. CONTROL CONTAINMENT 541 public CWi ndowlmpl , public IAxWinHostWindow, public 101eClientSite, public 101elnPIaceSi teWi ndowless t public 101eControlSite, public 101eContainer, publ i c IObjcctWi thS1 telmpl , public IServlceProvlder, public IAdviseSink, #ffndef J^TLJIOJ)OCHOS*niIHAMDLER public IDocHostUIHandler, #end1f public XOispatchlmpl { ■ • • LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM IParam, B00L& private: CWindow m_ax; The creation of the CAxHostWi ndow object and the corresponding control is initiated in the WM_CREATE handler of the AtlAxWin window procedure, A tl- AxWi ndowProc: static LRESULT CALLBACK AtlAxWindowProc(HWNO hWnd, UINT uMsgf WPARAM wParamf LPARAM IParam) { switchCuHsg) { case WPLCREATE: { II create control from a PROCID in the title // This is to make sure drag drop works ::01eInitiali2e(NULL); IResult) { // Create the host window, the CAxHostWindow object, and II the BullsEye control, and host the control RECT r«ct; CetCl1entRect(4r«ct); LPCTSTR pszNaac = _ TC'ATanternal s. Bui 1 sEye"); HWNO hwndContainer = ■.ax.CreateC—TC'AtlAxWin"), R_hWnd( rect, pszNaae, WS-CHILD | WS.VISIBLE); if( IhwndContainer ) return -1; return 0; 544 ATL INTERNALS CREATESTRUCT* lpCreate * (CREATESTRUCT*)1Param; int nLen * ::CetWindowTextLength(hWnd); LPTSTR IpstrName • (LPTSTR) 一 alloca((nLen + 1> * sizeof(TCHAR)); // Extract window text to be used as name of control to host ::GetWi ndowText (hWnd, IpstrNaM, nL«n + 1); ::SetWindowText(hWnd, _T ( " “)) ; IAxWinHostWindow* pAxWi ndow * NULL; « » « USES—CONVERSION; CComPtr spUnk; // Create CAxHostWindow instance and host the control HRESULT hRet * AtlAxCreateControl(T2G0LE(IpstrName)( HWndf spStreaa, &spUnk); if(FAILED(hRet)) return -1; // abort window creation hRet » spUnk->Que rylnte rface(IIO_IAxWi nHostWi .idow t (void**) ApAxWindow); if(FAILED(hRet)) return -1; // abort window creation // Keep a CAxHostWindow interface in the window's user data ::SetWi ndowLong ChWnd, GWU.USCROATA, (DWORD) pAxW1 ndow); • •暑 II continue with DefWindowProc } 建 讓 . break; case WM.NCOESTROY: { IAxWi nHostWi ndow* pAxWi ndow * (IAxWinHostWindow*)::GetWindowLong(hWndf CWL_ USERDATAil? // When window goes away, release the host (and the control) 1f(pAxWindow f« NULL) pAxW1ndoM->Re1easc(); 01 eUni rvi ti al i ze Q i ; } break; > CONTROL CONTAINMENT 545 return ::DefWindowProc(hWnd, uMsg, wParam, 1Param); > . Notice that the window’s text, as passed to the call to CWi ndow::C reate,is used as the name of the control to create. The call to A tlA x C reateC ont ro l, passing the name of the control, forwards to AtlAxCreateCont rol Ex, which furthers things by creating a CAxHostWi ndow object and asking it to create and host the control: ATLINLINE ATLAPI AtlAxCreateControlEx( LPCOLESTR IpszName, HWND hWnd. IStream* pStreara, IUnknown** ppUnkContainer, IUnknown** ppUnkControl• REFIID iidSink, IUnknown* punkSink) { AtlAxWinlnit(); HRESULT hr; CComPtr spUnkContainer; CComPtr spUnkControl; hr « CAxHostWindow::MCreatorC1ass::CreateInstance(NUtLt IlOJCUnknoivn, C void* A)lr$pUnkContai ner); if (SUCCEEOED(hr)) { CComPtr pAxWindow; SpUnkContainer->QueryInterface(IIO«IAxWinHostWindow, (void**) ApAxWi ndow); CComBSTR bstrName(IpszName); hr * pAxWirnk>M->CreateControlEx(bstrNa»e, hWnd, pStreaa, IrspUnkControl t IidSink, punkSink); } « 參《 return hr; } lAxWinHostWindow A tl AxCreateControl Ex uses the IAxWi nHostWi ndow interface to create the control. IAxWi nHostWi ndow is one of the few interfaces that ATL defines and one 546 ATL INTERNALS of the interfaces that CAxHostWi ndow implements. Its job is to allow for manage­ ment of the control that it’s hosting: interface IAxW1nHostW1ndow : IUnknown { HRESULT CreateControHfin] LPCOLESTR lpTricsOata, [in] HWND hWndt [io3 IStream* pStream); HRESULT CrcateControlE^CCin] LPCOLESTR lpTricsOata, [in] HWNO hWnd, [in] IStreaw* pStream, [out] IUnknown** ppUnk, [in] REFIID riidAdvise, [in] IUnknown* punkAdvise); HRESULT AttachControKfin] IUnknoium* pUnkControl f [in] HWND HWnd); HRESULT QueryControl([in] REFIID riid, [out, iid 一 is(riid)] void ♦♦ppvObject); HRESULT SetExternalOispatch([in] IDispatch* pOisp); HRESULT SctExtcrnalUIHandler([in] IDocHostUIHandlerOi spatch* pOisp); To create a new control in the CAxHostWi ndow, IAxWi nHostWi ndow provides CreateControl or CreateControl Ex, which is what AtlAxCreateControl Ex uses after the CAxHostWi ndow object is created The parameters for Create­ C o n tro l [Ex] are as follows: ■ 1 pT ricsD ata. The name of the control to create. Can take the form of a CLSID, a ProgID,a URL, a file name, or raw HTML. I’ll discuss more about this later. ■ hWnd. Parent window in which to host the control. This window will be sub­ classed by CAxHostWindow. ■ pStream. Stream that holds object initialization data The control will be ini­ tialized via IPersi stStream lni t. If pStream is non-NULL, Load will be called. Otherwise, In i tNew will be called. ■ ppUnk. Will be filled with an interface to the newly created control. ■ riidAdvisc. If not IID 一 NULL, CAxHostWi ndow will attempt to set up a con­ nection point connection between the control and the sink object represented by the punkAdvi se parameter. CAxHostWi ndow will manage the resultant cookie and tear down the connection when the control is destroyed. ■ punkAdvi se. An interface to the sink object that implements the sink interface specified by r i i dAdvi se. The A tta c h C o n tro l method of the IAxWi nHostWi ndow method attaches a control that has already been created and initialized to an existing CAxHostWi ndow CONTROL CONTAINMENT CreateControlEx(STDMETHODIMP CAxHostWindow: LPCOLESTR IpszTricsData, HWNO hWnd, IStream* pStream, IUnknown** ppUnk, REFIID iidAdvise, ' V.' \ 、 . 1.**• \ V w*' * • ' * 'T*'»,1 A '.' ' y * v , IUnknown* punkSink) { HRESULT hr - S.FALSE; object The QueryControl method allows access to the contrors interfaces being hosted by the CAxHostWi ndow object. Both SetExternal Di spatch and SetEx- ternalU IH andler are for use when hosting the Internet Explorer HTML control and will be discussed at the end of the chapter in the HTML Controls section. CreateControl Ex CAxHostWi ndow’s implementation of CreateControl Ex subclasses the parent window for the new control, creates a new control, and then activates it. If an ini­ tial connection point is requested, A tlA dvi se is used to establish that connection. If the newly created control is to be initialized from raw HTML or to be navigated to via a URL, CreateControl Ex does that, too: // Release previously held control // Route all Messages to CAxHostWindow object // Create control based on IpszTricsData hr *= CreatcNonuil iz«d0bject(1pszTr1cs0at«, IIDJlUnkfioim, ^>o1d“)ppUnk, bWasHTML); bool blnited « hr S.FAtSE; II Activate tht control• (SUCCEEOtOChr)) Hr _ Act1v«teAx(*i>pUnk, bXnlUKl, p S t r m ) 548 ATL INTERNALS // Try to hook up any sink the user wight have given us. «.1^dS1nk * UdAdvise; 1f(SUCCEE0ED(hr) M «ppUnk M punkSink) At1Advise(*ppUnk, punkSink, «.1idS1nk, teL.dwAdvi seSi nk); // If raw HTML, give HTMt control its HTML II If it’s an URL, navigate the Browser control to the URL if (FAILED(hr) 11 m.spUnknown «■* NULL) { // We don't have a control or something failed, so release ReleaseAll(); > return hr; « How the control name is interpreted depends on another function called Create- Normal i zedO bject. The activation is handled by the A c ti vateAx function. CreateNormalizedObject The CreateNormal i zedObject function will create an instance of a COM object using strings of the form shown in Table 11.1. Because CAxHostWi ndow uses the title of the window to obtain the name passed to CreateNormal i zedObject, you Table 11.1. String Formats Understood by CreateNormal i zedObject Type Example(s) CLSID of created object HTML mshtmI:Wow! CLSIDJTMLDocument CLSID (7DC59CC5-36C0-1 lD2-AC05>O0A0C9C8E50D} Result of CLSIDFromStri ng ProgID ATLIntemals.BullsEye Result of CLSIDFromProgID URL http://www.awl.com res^/htmlapp.exe/main.htm CLSID一WebB rowse r Active D:\Atl IntemalsMl Control Containment docInternals' ):\Atl Intedocument file^/D:\Atl IntemalsXlO Controls.doc CLSID_WebB rowse r CONTROL CONTAINMENT 549 can use any of these string formats when creating an instance of the AtlW i n ln i t window class. ActivateAx The A cti vateAx function is the part of the control creation process that really performs the magic. It takes an interface pointer from the object that Create- Normal i zedObject creates and activates it as a COM control in the parent win­ dow. A c ti vateAx is responsible for the following: ■ Setting the client site, that is, the CAxHostWi ndow’s implementation of 101 e- C1 i entSi te, via the contrors implementation of 101 eObject. ■ Calling either In i tNew or Load (depending on whether the pStream argument to A tl AxCreateControl Ex is NULL or non-NULL) via the contrors implemen­ tation ofIPersistStream lnit. ■ Passing the CAxHostWi ndow’s implementation of IA dviseSink to the con- trol’s implementation of IV i ewObject. ■ Setting the contrors size to the size of the parent window, also via the contrors implementation of 101 eObject. ■ Finally, to show the control and allow it to handle input and output, calling DoVerb(OLEIVERB_INPLACEACTIVATE) via the contrors implementation of, again, 101 eObject. This process completes the activation of the control. However, creating an in­ stance of CAxHostWi ndow via direct reference to the AtlAxWin window class is not typical. The implementation details of AtlAxWin and CAxHostWi ndow are meant to be hidden from the average ATL programmer. The usual way a control is hosted under ATL is via an instance of a wrapper class called CAxWi ndow. CAxWindow CAxWi ndow simplifies the use of CAxHostWi ndow with a set of wrapper functions. The initial creation part of CAxWi ndow class is defined as follows. template class CAxWindowT : public TBase { public: // Constructors CAxW1ndowT(HWN0 hWnd * NULL) : TBaseChWnd) {} CAxWindowT< TBase >A operator*(HWND hWnd) { m_hWnd * hWnd; return *this; } 550 ATL INTERNALS // Attributes static LPCTSTR C«tWndClassNane() { return .TC'AtlAxWin"); } // Operations HWND Create(HWND hWndParent, RECTA rc, LPCTSTR szName j { return CWi ndow::Create(CetWndClassNaacC)• hWndParent szName, •••); > HWNO Create(HWNO NULL. hWndParent, LPRECT lpr - NULL, LPCTSTR return CWindow::Create(CetWndClassNa»e()t hWndParent t )-Nzs ■ 馨激 NULL rc. szName lpr. 法 遂 參 絷 、..:¥ 总 :• US ' 〈•、•• 故 泰■ : . 欲 typ«d«f CAxW1ncloMT CAxWifidoiv; Notice that both Create functions still require the parent window and the name of the control, but do not require passing the name of the CAxHostWi ndow window class. Instead, CAxWi ndow knows the name of the appropriate class itself (available via the static member function CetWndCl assName) and passes it to the CWi ndow base class just like we had done manually. Using CAxWi ndow reduces the code re­ quired to host a control to the following: class CMai nWi ndow : public CWi ndowlmpl { • •暴 LRESULT OnCreate(UINT uMsgr WPARAM wParam, LPARAM 1Param, B 0 0 L & IResult) { II Create the host window, the CAxHostWindow object, and // the BullsEye control, and host the control RECT rect; CetCli entRectCArect); LPCTSTR pszName * 一 T("ATLInternals.Bui1sEye"); HWND hwndContainer * n_ax.Create(«_hWnd t rect, pszName, WS_CHILO 丨 WS_VISIBLE); if( !hwndContainer ) return -1; return 0; private: CAxMlndow «.ax; CONTROL CONTAINMENT The combination of a custom window class and a CWi ndow-based wrapper pro­ vides exactly the same model as the window control wrappers that I discussed in Chapter 9. For example, EDIT is a window class, and the CEdi t class provides the client-side wrapper. The implementation of the EDI丁 window class happens to use the window text passed via C reateW i ndow as the text to edit. The At 1 AxWi n class, on the other hand, uses the window text as the name of the control to create. The job of both wrapper classes is to provide a set of member functions that replace calls to SendMessage. CEdi t provides member functions like CanUndo and Cet- Li neCount, which send the EM_CANUNDO and EM_GETLINECOUNT messages. CAx- Wi ndow, on the other hand, provides member functions that also send window messages to the AtlAxWi n class (which I’ll discuss later). The only real difference between EDIT and A tl AxWi n is that EDIT is provided with the operating system, whereas A tl AxWi n is only provided with ATL.4 Two-Step Control Creation You may have noticed that A tl AxC reateCon t ro l Ex takes some interesting param­ eters, such as an I St ream interface pointer and an interface ID/interface pointer pair to specify an initial connection point. However, while the window name can be used to pass the name of the control, there are no extra parameters to Cre­ ateWi ndow for a couple of interface pointers and a globally unique identifier (GUID). Instead, CAxWi ndow provides a few extra wrapper functions, namely, CreateControl and CreateControl Ex: template HRESULT Cr«at«Contro1(DWORD dwResID, IStream* pStream • NULL, IUnknown** ppUnkContainer « HULL) { / / bstrURL URL of the form "reK/AfRoduleV^KvResIIV ATLASSERT(:: IsWi ndow(m_hWnd)); return AtlAxCreateControl(bstrURL, m_hWnd, pStream, ppUnkContainer); > 4 Arguably, a window class whose function is to host COM controls should be part of the OS. 552 ATL INTERNALS HRESULT CreateControlEx(LPCOLESTR IpszName. IStream* pStream ■ NULL IUnknown** ppUnkContainer * NULL, IUnknownppUnkControl « NULL, REFIID iidSink - IID^NULL, IUnknown* punkSink ■ NULL) { ATLASSERTC ::IsWindow(m.hWnd)); return AtlAxCreateControlExClpszName, m_hWnd, pStream, ppUnkContainer, ppUnkControl, iidSink, punkSink); } HRESULT CreateControlEx(OWORD dwResID, IStream* pStream • NULL, IUnknown** ppUnkContainer * NULL, IUnknown** ppUnkControl « NULL, REFIID iidSink • IID_NULL. IUnknown* punkSink ■ NULL) { // bstrURL •* URL of the form '*res:///" ATLASSERT (••: IsWi ndow(m_hWnd)); return AtlAxCreatcControlEx(bstrURL, m_hWnd, pStream, ppUnkContainer, ppUnkControl, iidSink, punkSink); } CreateControl and CreateControl Ex allow for the extra parameters that A tl AxCreateControl Ex supports. The extra parameter that the CAxWi ndow wrappers support beyond those passed to A tl AxCreateControl Ex is the dwRes­ ID parameter, which serves as an ID of an HTML page embedded in the resources of the module. This parameter will be formatted into a string of the format res:/// before being passed to AtlAxCreate- C ontrolEx. These functions are meant to be used in a two-stage construction of first the host and then its control. For example: LRESULT CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM IParam, BOOL& IResult) { RECT rect; CetClientRectC&rect); // Phase one: Create the container HWND HwndContainer = «_ax.Cr«ate(n.hWnd( rect, 0, WS.CHILD ) WS.VISIBLE); ifC IhwndContainer ) return -1; CONTROL CONTAINMENT 553 // Phase two: Create the control LPCOLESTR pszNaM = OLESTRC'ATLInternal s. Bui 1 sEye"); HRESULT hr = *_ax.CreateControl(pszNaiie); // This leaks, keep // reading! return (SUCCEEDED(hr) ? 0 : -1); 1,11 show you how to persist a control and how to handle events from a control later in this chapter. If you’ve already created a control and initialized it, you can still use the hosting functionality of ATL by attaching the existing control to 狂 host window via the AttachCont rol function: template 〈class TBase » CWindow> class CAxWindowT : public TBase { p u b lic : HRESULT AttachControl(IUnknown* pControl• IUnknown** ppUnkContai ne r){ ATLASSERT(:: IsWi ndowCfiL-hWnd)); return AtlAxAttachControl(pControl, nuhWnd, ppUnkContai ner): AttachControl is meant to be used like so: LRESULT CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lPara/n, BOOL& 1 Result) { RECT rect; GetClientRectC&rect); // Phase one: Create the container HWND hwndContainer = m_ax.Create(m_hWndf rect, 0, WS_CHILD | WS—VISIBLE); if( !hwndContainer ) return -1; // Create and Initialize a control CG)«Ptr spunkControl; // // Phase two: Attach an existing control HRESULT hr ■ bl.«x .AttachControl(spunkControl); // This also 1«aks! return (SUCCEEDED(hr) ? 0 : -1); 554 ATL INTERNALS Avoid CAxWindow Functions CreateControl, CreateControlEx, and AttachControl As attractive as the CreateControl, CreateControlEx, and AttachControl member functions are, however, as of ATL 3.0,you should avoid using them. They leak. Phase one creates an instance of CAxHostWi ndow, even if the window text is empty. All three of the second-phase control creator functions also create an in­ stance of CAxHostWi ndow and never destroy the first one. You’re leaking the size of a CAxHostWi ndow object (224 bytes) every time you call one of these three functions. Luckily, there’s a workaround. If you use the QueryHost member function of CAxWi ndow, you can obtain the IAxWi nHostWi ndow interface on the existing CAxHostWi ndow object: tempi ate class CAxWindowT : public TBase { public: HRESULT QueryHost(REFIID iid, void** ppUnk) { CComPtr spUnk; HRESULT hr * AtlAxCetHost(m_hWnd, &spUnk); if (SUCCEEDED(hr)) hr *= spUnk->QueryIntcrface(iid; ppUnk); return hr; > template 〈class Q> HRESULT QueryHost(Q** ppUnk) { return QueryHost(一 uuidof(Q), (void**)ppUnk): } ■■■■■■■■■■■■■■■■■■■■I The QueryHost member function uses the A tl AxCetHost function to send a cus­ tom window message to the A tl AxWi n window to obtain an interface pointer on the host Once you have the IAxWi nHostWi ndow interface, you can call the inter­ face member functions CreateControl, CreateControlEx, or AttachControl without worry of leaking because now you’re reusing the existing CAxHostWi ndow instead of creating a new one. For example: LRESULT CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM IParam, B00L& IResult) { RECT rect; GetClientRect(Arect); // Phase one: Create the container HWND hwndContainer = m_ax.Create(m_hWnd, rect, 0, WS_CHILD | WS_VISIBLE); ifC IhwndContainer ) return -1; CONTROL CONTAINMENT 555 // Phase two: Create the control in the existing container CComPtr spAxWindow; HRESULT hr = m_ax.QueryHost(&spAxWindow); FAILED(hr) ) return -1; LPCOLESTR pszName = OLESTRC'ATLIntarnals.BullsEye"); hr = spAxWinck>w~>CreateControl(pszName, n_ax.m.hWnd, 0); if( FAILED(hr) ) return -1; return 0; Because it’s a pain to do this all manually, the source code provided for this book contains a fix for CAxWi ndow called CAxWi ndow2.5 CAxWi ndow2 derives from CAxWi ndow and provides implementations of CreateControl, CreateControl - Ex, and A tta c h C o n tro l that reuse the existing CAxHostWi ndow object, if one is available, instead of creating a new one. On the other hand, if the m一 hwnd mem­ ber of the CAxWi ndow2 object has not been subclassed by an instance of CAxHost­ Wi ndow, one will be created using the same techniques that CAxWi ndow uses. For example, the CreateControl Ex function is implemented like so: HRESULT CAxWindowT2: LPCOLESTR IStream* IUnknown** IUnknown** REFIID IUnknown* { ATLASSERTC: :CreateControlEx( 1pszName, pStream * NULL, ppUnkContainer * NULL ppUnkControl = NULL, iidSink = IID_NULLf punkSink • NULL) :IsWindow(m_hWnd)); HRESULT hr = E^FAIL; CComPtr spunkControl; sThe name of the file that defines CAxWi ndow2 is axwi n2 • h. 556 ATL INTERNALS hr = spAxWindow->CreateControlEx(1pszName, m_hWnd, pStream, &spunkControlf iidSink, punkSink); i f( FAILED(hr) ) return hr; ifC ppUnkControl ) (^ppUnkControl = spunkControl)->AddRef(); if( ppUnkContainer ) C*ppJnkContainer = spAxWindow)->AddRef(); } // Create a new CAxHostWi ndow else { return AtlAxCreateControlEx(1pszName, m_hWnd, pStream, ppUnkContainer, ppUnkControl, iidSink, punkSink); } return S_OK; } typedef CAxWindowT2 CAxWindoM2; The CAxWi ndow2 class allows you to use the CAxWi ndow member functions Cre­ ateControl, CreateControl Ex and AttachControl as a replacement for CAx­ Wi ndow, but without the leak. Using the Control Once you've created the control, it’s really two things: a window and a control. The window is an instance of A tl AxWi n and hosts the control, which may or may not have its own winduw (CAxHostWi ndow provides full support for windowless con­ trols). Since CAxWi ndow derives from CWi ndow, you can treat it like a window (such as move it, resize it, hide it); A tl AxWi n will handle those messages by trans- lating them into the appropriate COM calls on the control. For example, if you’d like the entire client area of a frame window to contain a control, you can handle the WM—SIZE message like so: class CMai nVs!i ndow : public CWi ndowlmpl { • • • LRESULT OnSize(UINT, WPARAM, LPARAM IParam, BOOL&) { if( m_ax ) { // m_ax will be Created earlier, for example WM_CREATE RECT rect = { 0, 0 , LOWORD(IParam), HIWORD(IParam) }; m_ax.MoveWindow(&rect); // Resize the control } return 0; CONTROL CONTAINMENT private: CAxWindow m_ax; However, unlike Windows controls, COM controls do not accept functionality requests via Windows messages (remember, a COM control may not even have a window). Instead, becausc COM controls are COM objects, they expect to be pro­ grammed via their COM interface(s). To obtain an interface on the control, CAx­ Wi ndow provides the QueryControl method: template class CAxWindowT : public TBase { public: • • • HRESULT QueryControl(REFIID iid, void** ppUnk) { CComPtr spUnk; HRESULT hr * AtlAxCetControl(m_hWnd, AspUnk); if (SUCCEEOEO(hr)) hr * spUnk->QueryInterface(iid, ppUnk); return hr; > template HRESULT QueryControl(Q** ppUnk) { return QueryControl(一 uuidof(Q), (void**)ppUnk); } Like QueryHost, QueryControl uses a global function (AtlAxCetControl in this case) that sends a window message to the A tl AxWi n window to retrieve an in­ terface, but this time from the hosted control itself. Once the control has been cre­ ated, QueryControl can be used to get at the interfaces of the control: // Import interface definitions for BullsEye #1_port "D:\ATLBook\src\at1interna1s\0ebug\Bul1sEyeCt1.dll" \ ra w _interface s.o nly raw 一 nat1ve_types no^naaespacc nauaecLgulds LRESULT CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM IParam, 600L& IResult) { // Create the control // S«t Initial BullsEye properties CCoaPtr spBul1sEye; ATL INTERNALS HRESULT hr = m_ax.QueryControl(&spBul1sEye); if( SUCCEEDEDChr) ) { spBul1sEye->put_Beep(VARIANT_TRUE); spBul1sEye->put_CenterColor(RGB(0, 0, 255)); } return 0; Notice the use of the #i mport statement to pull in the definitions of the inter­ faces of the control you're programming against. This is necessary if you only have t.ho control’s server DLL and the bundled type library but no original IDL (a common occurrence when programming against controls). Notice also the use of the #im- p o r t statement attributes, for example, raw_i n te r fa c e s —on l y. These attributes are used to mimic as closely as possible the C++ language mapping you would have gotten had you used mi d l . exe on the server’s IDL file. Without these attributes, Vi­ sual C+ + will create a language mapping that uses the compiler-provided wrapper classes (such as _bstr_t, 一 vanant-t, and —com _ptr_t),which are different from the ATL-provided types (such as CComBSTR, CComVariant, and CComPtr). While the compiler-provided classes have their place, I find it’s best not to mix them with the ATL-provided types if I can help it. Apparently the ATL team agrees with me, because the ATL Wizard-generated #im port statements also use these attri­ butes. (I’ll talk more about the control-contairunent-related wizards later.) Sinking Control Events Not only are you likely to want to program against the interfaces that the control im­ plements, but you’re also likely to want to handle events fired by the control. Most controls have an event interface, which, for maximum compatibility with the largest number of clients, is often a di spi nterface.6 For example, the BullsEye control from the last chapter defined the following event interface: const int DISPID_ONRINCHIT = 1; const int DISPID_ONSCORECHANGED = 2; dispinterface 一 IBullsEyeEvents { properties: methods: [id(DISPID一 ONRINGHIT)] void OnRingHit(short ringNumber); [id(DISPID一ONSCORECHANGED)] void OnScoreChanged(long ringValue); eThe scripting engines that Internet Explorer (IE) hosts will only allow you to handle events defined in a dispinterface. CONTROL CONTAINMENT For a control container to handle events fired on a d i spi n te r fa c e would require an implementation of ID i spatch. Implementations of IDi spatch are easy if the interface is defined as a dual interface, but much harder if it is defined as a raw di spi nterface.7 However, as you recall from Chapter 8, ATL provides a helper class called IDi spEventlmpI for implementing an event di spi nterface: template class AT 匕 NCLVTABLE IDispEventlnpl : public IOispEventSimpleImpl {•••}; ID i spEventlmpI uses a data structure called a sink map established via the fol­ lowing macros: ♦define BECIN3INICMAP(^c1 ass) ... d e fine SINietNTRY^INFO(id, iid, dispid, fn, info) ... fdefine SIN»CENTRY.EX(id, iid, dispid. fn) ... #define SIN»eEHTRYCid, dispid, fn) ... #define END.SINKJ4APC) ... The gory details of these macros are explained in Chapter 8, but the gist is that the sink map provides a mapping between a specific object/i i d/di spi d that defines an event and 汪 member function to handle that event. If the object is a nonvisual one, the usage of the sink map can be a bit involved. However, if the object is a> COM con­ trol, usage of ID i spEventlm pI and the sink map are quite simple, as you’re about to see. To handle events, the container of the controls will derive from one instance of IDi spEventlmpI per control. Notice that the first template parameter of ID i sp­ Eventlm pI is an ID. This ID will match to the contained control via the child win­ dow ID, that is, the nID parameter to Create. This same ID will be used in the sink map to route events from a specific control to the appropriate event handler. The child window ID is what makes ID i spEventlm pI so simple in the control case. Nonvisual objects have no child window ID and the mapping is somewhat more difficult (although, as Chapter 8 described, is still entirely possible). So, handling the events of the BullsEye control merely requires an ID i sp­ EventlmpI base class and an appropriately constructed sink map: 7 ATL’s ID i spatchlm pl can only be used to implement dual interfaces. 560 ATL INTERNALS const UINT ID.BULLSEYE = 1; class CMai nWi ndow : public CWi ndowlmpl , public IDispEventlxpl { public: • • • LRESULT OnCreate(...) { RECT 「ect; GetClientRect(&rect); m_ax.Create(m_hWnd, 「ect, — T("AtlInternal s.BuiIsEye"), WS 一 CHILD 丨 WS—VISIBLE, 0, ID.BULLSEYE); • • • return (m_ax.m_hWnd ? 0 : -1); BECIN_SINK_MAPCCMainWindow) SINK-ENTRY_EX(ID_BULLSEYE, DIID 一 IBul1sEyeEvents, 1, OnRingHit) SINK_ENTRY_EXCID.BULLSEYE, DIID 一 IBul1sEyeEvents, 2, OnScoreChanged) END_SINICMAP() void — stdcall OnRingHit(short nRingNumber); void 一 stdcall OnScoreChanged(LONG ringValue); private: CAxWindow m_ax; }; Notice that the child window control ID (ID_BULLSEYE) is used in four places. The first is the ID i spE ventlm pl base class. The second is the call to C reate, marking the control as the same one that will be sourcing events. The last two uses of ID_ BULLSEYE are the entries in the sink map, which route events from the ID_BULLS- EYE control to their appropriate handlers. Notice also that the event handlers are marked 一 stdcall. Remember that we're using IDi spEventlmpl to implement IDi spatch for a specific event inter­ face (as defined by the D IID 一 IB u l ^ sEyeEvents interface identifier). That means that ID i spEventlm pl must unpack the array of VARIANTS passed to Invoke, push them on the stack, and call our event handler. It does this using type information at runtime, but, as mentioned in Chapter 8,it still has to know about the calling CONTROL CONTAINMENT 561 convention, that is, in what order the parameters should be passed on the stack and who’s responsible for cleaning them up. To alleviate any confusion, ID i sp­ Eventlmpl requires that all event handlers have the same calling convention, w hich_ s td c a l 1 defines. Once we have ID i spE ventlm pl and the sink map set up, we’re not done. Un­ like Windows controls, COM controls have no real sense of their “parent•” This means that instead of implicitly knowing to whom to send events, like an edit con­ trol does, a COM control must be told who wants the events. Because events are es­ tablished between controls and containers with the connection point protocol, somebody has to call Querylnterface for IConnectionPointContainer, call FindConnectionPoint to obtain the IConnectionPoint interface, and finally call Advi se to establish the container as the sink for events fired by the control. For one control, that’s not so much work, and ATL even provides a function called A tl - Advi se to help. However, for multiple controls, it can become a chore to manage the communication with each of them. And since we have a list of all the controls with which we’d like to establish communications in the sink map, it makes sense to leverage that knowledge to automate the chore. Luckily, we don’t even have to do this much, because ATL’s already done it for us with A tl Advi seSi nkMap: template inline HRESULT At1Adv1seSinkMap(T* pT, bool bAdvise) The first argument to A tl Advi seSi nkMap is a pointer to the object wishing to set up the connection points with the objects listed in the sink map. The second pa­ rameter is a Boolean determining if we are setting up or tearing down communica­ tion. Because A tl Advi seSi nkMap depends on the child window ID to map to a window that already contains a control, both setting up and tearing down connec­ tion points must occur when the child windows are still living and contain controls. Handlers for the WM_CREATE and WM_DESTROY messages are excellent for this purpose: LRESULT CMainWi ndow::OnCreate(.•.) { … // Create the controls AtlAdviseSinkMap(tH1sf true); // Establish connection points return 0; LRESULT CMainWindow::0nDestroy(...) { II Controls still live AtlAdvi seSi nkMapCthi s, falsc); // Tear down conncction points return 0; } 562 AT L INTERNALS The combination of IDi spEventlmpI, the sink map, and the A tl Advi se­ Si nkMap function are all that is needed to sink events from a COM control. How­ ever, we can further simplify things. Most controls implement only a single event interface and publish this fact in one of two places. The default source interface can be provided by an implementation of IProvi deC lassInfo28aiul can be published in the cocl ass statement in the IDL (and, therefore, as part of the type library). For example: coclass BullsEye { [default] interface IBullsEye; [default, source] dispinterface _IBul1sEyeEvents; In the event that ID i spEventlmpI is used with IID —NULL as the template pa­ rameter (which is the default value) describing the sink interface, ATL will do its best to establish communications with the default source interface via a function called AtlCetObjectSourcelnterface. This function will attempt to obtain the object's default source interface, using the type information obtained via the Get- T y pelnfo member function of ID i spatch. It first attempts the use of IP ro v i de­ Cl a s s ln f o2; if that’s not available, it will dig through the cocl ass looking for the [defaul t , source] interface. The upshot is that if you want to source the default interface of a control, the parameters to ID i spEventlmpI are fewer and you can use the simpler SINK_ENTRY. For example, the following is the complete code nec­ essary to sink events from the BullsEye control: #import "D:\ATLBook\src\atlinternals\Debug\Bul1sEyeCt1.dll" \ raw_interfaces_only raw_native^types no_namespace named_guids #define ID3ULLSEYE 1 class CMai nWi ndow •• public CWi ndowlmpl, // Sink the default source interface public IDi spEventlmpI< ID_BULLSEYE, CMainWindow> { LRESULT OnCreateC...) { RECT rect; GetClientRectC&rect); 'The scripting engines that Internet Explorer hosts will only allow you to handle events on the default source interface as reported by IProvi deCl asslnf o2. CONTROL CONTAINMENT m^.ax. Create (m_hWnd, rect, _ T ( MAt1 Internal s. Bull sEye"), WS_CHILD | WS—VISIBLE , 0, ID_BULLSEYE) ; At1AdviseSinkMap(this, true); return (m_ax.m_hWnd ? 0 : -1); } LRESULT CMainWindow::0nDestroy(.•.) { AtlAdviseSinkMap(this, false); return 0; } BECIN_SINK_MAP(CMainWi ndow) // Sink events from the default BullsEye event interface SINK_ENTRY(ID_BULLSEYE, 1, OnRingHit) SINK»ENTRY(ID_BULLSEYE, 2, OnScoreChanged) END 一 SIIMAPO void 一 stdcall OnRingHit(short nRingNumber); void —stdcall OnScoreChanged(LONG ri ngValue); CAxWi ndow m_ax; Property Changes In addition to a custom event interface, controls will often source events on the IPropertyNoti fySi nk interface: interface IPropertyNotifySink IUnknown { y HRESULT OnChanged([in] DISPID dispIO); HRESULT OnRequestEdit([in] DISPID dispIO); A control uses the IPropertyNoti fySi nk interface to ask the container if it’s OK to change a property (OnRequestEdi t ) and to notify the container that a prop­ erty has been changed (OnChanged). OnRequestEdi t is used for data binding, which is beyond the scope of this book, but OnChanged can be a handy notifi­ cation, especially if the container expects to persist the control and wants to use OnChanged as an is-dirty notification. Even though IPropertyNotif y S ink is a connection point interface, it’s not a di spi nte r f ace, so neither ID i spEventlmpl ATL INTERNALS nor a sink map is required. Normal C+ + inheritance and A tl Advi se will do. For example: class CMainWindow : public CWindowlmpl, public IPropertyNotifySink { public: • •參 // IUnknown, assuning an instance on the stack STDMETHODIMP QuerylnterfaceCREFIID riid, void** ppv) { ifC riid == lID_IUnknown 11 riid =* IID_IPropertyNotifySi nk ) ★ppv = static_cast(this); else return *ppv =* 0, E_NOINTERFACE; return reinterpret_castC^ppv)->AddRef () , S_0K; } STDMETHODIMP_CULONC) AddRef() { return 2; } STDMETHODIMP_CULONC) ReleaseC) { return 1; } // IPropertyNoti fySi nk STDMETHODIMP OnRequestEdi t(DISPID dispID) { return S.OK; } STDMETHOOIMP OnChanged(DISPID dispID) { m^bOirty = true; return S_0K; } private: CAxControl m_ax; bool m_bDirty; You have two choices when setting up and tearing down the IProperty­ N oti fySi nk connection point with the control. You could use A tl Advi se after the control is successfully created and A tl Unadvi se just before it is destroyed. This requires management of the connection point cookie yourself. For example: LRESULT CMainWindow::0nCreate(...) { … // Create the control II Set up IPropertyNoti fySi nk connection point CCo«Ptr spunkControl; «_ax.QueryControl(spunkControl); AtlAdvise(spunkControl, this, IIO.IPropertyNotifySink. &m_dwCookie); return 0; CONTROL CONTAINMENT 565 LRESULT CMainWindow::OnDestroy(...) { // Tear down IPropertyNotifySInk connection point CCoaPtr spunkControl; •QueryControl(spunkControl); Atl Unadvise (spunkControl, IIDJPropcrtyNoti fySi nk, ta.dwCookie); return 0; } Choice number two is to use the CAxWi ndow member function CreateC ontrol Ex, which allows for a single connection point interface to be established and the cookie to be managed by the CAxHostWi ndow object This simplifies the code considerably: LRESULT CMainWindow::OnCreateC...) { … // Create the control host // Create the control and set up IPropertyNotifySi nk connection point •_ax. CreateControl Ex (OLESTR ('* Atl Internal s. Bui 1 sEye"), 0, 0, 0, IID„IProp«rtyNotifySink, this); return 0; } The connection point cookie for IPropertyNoti fySink will be managed by the CAxHostWi ndow object, and when the control is destroyed, the connection will be tom down automatically. While this trick only works for one connection point interface, this technique combined with the sink m ap are likely all you’ll ever need when handling events from controls. Ambient Properties In addition to programming the properties of the control, you may wish to program the properties of the contrors environment, known as ambient properties. For this purpose, CAxHostWi ndow implements the IAxWi nAmbientDi spatch interface: interface IAxWinAablent01spatch : IDi spatch { [propput] HRESULT Al lonMindowlMsAct 1 v«t1on( [i n] VARIANT^OOL b); [propget] HRESULT Al 1 ovvWi ndowl essActi vati on ( [out, retval JVARIAMTJ00L* pb) // OISPIO^.AMBIEMT^BACKCOLOR [propput, id(OISPIOJW8IENT_BACKCOLOR>] HRESULT BackCo1or([in]0LE.C0L0R clrBackground); [propget• i d(OISPIDJ^M8IEWT^BACKCOLOR)] HRESULT BackColor([out,retval]0LE«C0L0R* pcirBackground) ATL INTERNALS // DISPIDJWBIENT.f [propput, 彳 d(OISPID^AMBIENT^FORECOLOR)] HRESULT ForcColor([in]OLE_COLOR clrForeground); [propget, id(DISPIDjMB I ENT—FORECOLOR)] HRESULT ForeColor([out,retval]0LE_C0L0R* pcirForeground) // DISPID_AMBIENT^LOCALEIO [propput, id(OISPID>^M8IENT^LOCALEIO)] HRESULT Local«IO([in]LCIO IcidLocalelO); [propget, id(01SPIO_AMBIENT_L0CALEID)] HRESULT LocaleID([out,retval]LCIO* plcidLocalelD); .USERMODI :t>—AMBier // DISPID_AMBIENT_l [propput. id(OISPIOJ\MBIENT_USERMOOE)] HRESULT tis«r«ode([in]VARIANT 一 BOOL bUserMode); [propget, id(0ISPIDJVM8IENT_USERM0DE)] HRESULT UserModeC[out,retval]VARIANT^BOOL* pbUserMode); II OISPIOJkMBIENT^OISPLAYASOEFAUtT [propput, id(OISPID^MBIENTJ)ISPLAYASDEFAULT)] HRESULT D1 邛UyAsD«fauU([in]VARIANT«_BOOL bDispiayAsDefault),A$0cfau1i :5PIDJVM8;[propget, idCDX5PIDjVM8IENT_DISPLAYASDEFAULT)] HRESULT Di spiayAsDefault([out,retval]VARIANT.BOOL* pbOispiayAsOefault); // DISPIDJVM8IENT一 FONT (propput, id(OISPID^AMBIENT^FONT)) HRESULT Font([in]IFontDisp* pFont); [propget, id(DISPID__AMBIENT_FONT)J HRESULT Font([out,retval]IFontOi spfr* pFont)i;; // OISPID^AMBIENT^H€SSACEREFLECT fpropput, id(0I$PI£UWIENTJ4€SSACEREFLECT)] ^ HRESULT Ptess49cR«n«ctC[in]VARXAHT.BOOL bMsgReflect); [propget, iil(OISPIO^AMBIENT_MESSACEREFLECT)] HRESULT MessageReflect([out,retval]VARIANT_BOOL# pbMsgReflect) // OISPID^AMBIENT.SHOWCRABHANOLES [propget, \d(DISPID^AM8IENT«SH0WGRABHANDl.ES)] HRESULT ShowCrabHandl€*(VARIANT.BOOL* pbShoivCrabHandles); CONTROL CONTAINMENT 567 H DISPIDJWBIENT3H0WHATCMIMC [propget,1 d (DISPIDJVMBIdSHOWHATCHINO 】 HRESULT ShoMltatchlngCVARlANTJSOOL* pbShowHatcM ng); // IDocHostUIHandler Defaults QueryHost can be used on a CAxWi ndow to obtain the IWi nAmbi entDi s patch interface so that these ambient properties can be changed. For example: LRESULT CMainWindow::OnSetGreenBackground(.••) { // S«t up gre«n aablent background CCoaPtr spAabient; hr * n.ax.QueryHost(AspAabltnt); ifC SUCCEEDEO(hr) ) { spAabient->put^ackCo1or(RGB(0l 25S, 0)); > return 0; } Whenever an ambient property is changed, the control is notified via its implemen­ tation of the 101 eControl member function OnAmbi entPropertyChange. The control can then Q uerylnterface any of its container interfaces for ID i spatch to obtain the interface for retrieving the ambient properties (which is why IAxWi n- Ambi entDi spatch is a dual interface). Hosting Property Pages If your container is a development environment, it’s possible that you'd like to allow the user to show the control’s property pages. This can be accomplished by calling the 101 eO bject member function DoVe rb, passing in the OLEIVERB^PROPERTIES verb ED: LRESULT CMainWindow::OnEditProperties(...) { CComPtr<101eObject> spoo; HRESULT hr ■ m.ax.QueryControlC&spoo); if( SUCCEEOEO(hr) ) { CComPtr<101eCli entSi te> spcs; m_ax.QueryHost C&spcs); RECT rect; m_ax.CetClientRect(Arect); ATL INTERNALS hr = spoo->DoVerb(OLEIVERB_PROPERTIES p 0, spcs, -1 , «_ax.m_hWnd, Arect): if( FAILED(hr) ) MessageBox(—TC'Properties unavailable"), 一 TC.Error")); > return 0; In the event that you’d like to add your own property pages to those of the con­ trol or you’d like to show the property pages of a control that doesn’t support the OLEIVERB_PROPERTIES verb, you can take matters into your own hands with a custom property sheet. First, you’ll need to ask the control for its property pages via the ISpeci fyPropertyPages member function GetPages. Second, you may want to augment the control's property pages with your own. Finally, you'll show the property pages (each a COM object with its own CLSID) via the COM global function OleCreatePropertyFrame, as demonstrated in the ShowProperties function I developed for this purpose: HRESULT ShowRpoperties(IUnknown* punkControl, HWND hwndParent) { HRESULT hr * E.FAIL; // Ask the control to specify its property pages CComQIPtr spPages = punkControl; if (spPages) { CAUUID pages; hr = spPages->G€tPages(&pages); if( SUCCEEDED(hr) ) { // TO DO: Add your custom property pages here CComQIPtr spObj * punkControl; ifC spObj ) { LPOLESTR pszTitle = 0; spObj->CetUserType(USERCLASSTYPE_SHORTf ApszTitlc); I I Show the property pages hr = OleCreatePropertyFrame(hwndParent, 10, 10, pszTitle, 1, &punkContro1, pages.cElenst p a g e s .p E le n s , LOCALE_USER_DEFAULTf 0. 0): CONTROL CONTAINMENT CoTaskMemFree(pszTi11 e); } CoTaskMemFree(pages.pElems); return hr: The ShowProperties function can be used instead of the call to DoVerb. For example: LRESULT CMainWindow::OnEditProperties(...) { CComPtr spunk; if( SUCCEEDED(m_ax.QueryControl(&spunk)) ) { if( FAILED(ShowProperties(spunk, m—hWnd)) ) { MessageBoxC_ T("Properties unavailable"), —T(”Error”)); > } return 0; Either way, if the contrors property pages are shown and the Apply or OK button is pressed, your container should receive IPropertyN oti f ySi nk calls, one per property that has changed. Persisting a Control It may be that the control’s state is something that you’d like to persist between ap­ plication sessions. As discussed in Chapter 6,this can be done with any number of persistence interfaces, of which most controls implement IPersi stS tream lnit (although IP ersi stStream is a comnion fallback). For example, saving a control to a file can be done with a stream in a structured storage document: bool CMainWindow::Save(LPCOLESTR pszFileName) { II Make sure object can be saved // Note: Our IPersi stStream interface pointer could end up holding an // IPersi stStreamlni t interface. This is OK since IPersistStream // is a layout-compatible subset of IPersi stStreamlnit. CComQIPtr spPersistStream; ATL INTERNALS HRESULT hr = m^ax.QueryControl(AspPersistStream); if( FAILEDChr) ) { hr = nuax.QueryControl(IID 一 IPersistStreamlnit, (voi d*A)&spPersi stStream); if( FAILED(hr) ) return false; } II Save object to stream in a storage CComPtr spStorage; hr = StgCreateOoc fi1 ft CpszFi1eNane, STCMJ)IRECT 丨 STQO^ITE 1 STGM_SHARE«EXCLUSIVE I STCM.CREATE, O, ftspStorage); if( SUCCEEDED(hr) ) { CComPtr spStream; hr = spStora9e->CreateStreaffiC0LESTRC,,Contents"), STCM.DIRECT | STCM一 WRITE | STGH_SHARE_EXCLUSIVE | STCH.CREATE, 0 , 0, irspStrean); if( SUCCEEDED(hr) ) { II Get and store the CLSID CLSID clsid; hr = spPersistStreaii->CetC1assXDCSfCls1d); if( SUCCEEDEDChr) ) { hr : spStpeaii->Wr1te(&clsidl sizeof(clsid), 0); // Save the object hr = spPersi stStream->SaveCspStream, TRUE); if( FAILED(hr) ) return false; return true; Restoring a control from a file is somewhat easier because both the Create­ Control and the CreateControlEx member functions of CAxWindow take an IStream interface pointer to use for persistence. For example: CONTROL CONTAINMENT bool CMainWindow::Open(LPCOLESTR pszFileName) { // Open object a stream in the storage CCon:Ptr spStorage; CComPtr spStream; HRESULT hr; hr = StgOpenStorageCpszFileName, 0, STCMJIRECT | STOLREAD \ STCM 一 SHARE 一 EXCLUSIVE • 0, 0, &spStorage); if( SUCCEEDEDChr) ) { hr = spStorage->Op€nStr'ea«(OLESTR("Contents"), 0, STCMJJIRECT 丨 STGM 一 READ | STCM_SHARE^EXCLUSIVE. 0, &spStreaai); if( FAILEDChr) ) return false; // Read a CLSID from the stream CLSID clsid; hr = spStream->Read(&c1sid, sizeof(clsid), 0); if( FAILED(hr) ) return false; RECT rect; GetClientRect(&rect); OLECHAR szC1sid[40]; StringFromCUID2(clsid, szClsid, lengthof(szClsid)); II Create the control*s host wi ndow if( !m_ax.Create(m_hWnd, rect, 0 , WS_CHILD | WS_VI5IBLE, 0, ID_CHIL0_C0NTR0L) { return false; } // Create the controlt persisting fr o n the stream hr = »_ax.CreateControl(szClsid, spStream); if( FAILED(hr) ) return false; return true; } When a NULL IStream interface pointer is provided to either CreateCon­ tro l or CreateControl Ex, ATL will attempt to call the IPersi stStream lni t 572 ATL INTERNALS member function In i tNew to make sure that either In i tNew or Load is called, as appropriate. Accelerator Translations It’s common for contained controls to contain other controls. In order for keyboard accelerators (such as the tab key) to provide for navigation between controls, the main message loop must be augmented with a call to each window hosting a con­ trol to allow it to pretranslate the message as a possible accelerator. This function­ ality must ask the host of the control with focus if it wants to handle the message. If the control does handle the message, no more handling need be done on that mes­ sage. Otherwise, the message processing can proceed as normal. A typical imple­ mentation of a function to attempt to route messages from the container window to the control itself (whether it's a windowed or a windowless control) is shown here: BOOL CMainWnd:: PreTranslateAccelerator(MSG* pMsg) { // Accelerators are only keyboard or mouse messages if ((pMsg->message < WM_KEYFIRST 丨丨 pMsg->message > WM_KEYLAST) && (pMsg->message < WM_MOUSEFIRST 11 pMsg->message > WM_MOUSELAST)) return FALSE; II Find a di rect child of this window from the wi ndow that has // focus. II This will be AxAtlWin wi ndow for the hosted control. HWND hWndCtl = ::CetFocus(); if( IsOvnd(hWndCtl) && ::CetParent(hWndCtl) !- m_hWnd ) { do hWndCtl = ::CetParent(hWndCtl); whileC ::CetParent(hWndCt1) != m 一 hWnd ); } // Give the control (via the AtlAxWin) a chance to translate // this message if (::SendMessage(hWndCtl, WM.FORWARDMSG, 0, (LPARAM)pMsg)) return TRUE; // Check for dialog-type navigation accelerators return IsDialogMessage(pMsg); } The crux of this function forwards the message to the AtlAxWin via the WM_F0RWARDMSC message. This message is interpreted by the host window as CONTROL CONTAINMENT an attempt to let the control handle the message if it so desires. This message will be forwarded to the control via a call to the 101 elnPI aceActi veObj ect mem­ ber function Transl ateAccel erator. The PreTransl ateAccel erator func­ tion should be called from the application’s main message pump like so: int WINAPI WinMain(...) { • • • CMainWindow wndMain; HACCEL haccel * LoadAccelerators(_Module.CetResourceInstance(), MAKEINTRESOURCE(IDC.MYACCELS)); MSG msg; whileC GetMessageC&msg, 0, 0, 0) ) { if( !TranslateAccelerator(msg.hwnd, haccel, &msg) && !wndMain.PreTranslateAcceleratorC&msg) ) { Trans')ateMessage(Amsg); DispatchMessage(Amsg); The use of a PreT ransl ateAccel erato r function on every window that contains a control will give the keyboard navigation keys a much greater chance of working, although the individual controls have to cooperate too. Hosting a Control in a Dialog Inserting a Control into a Dialog Resource So far, I’ve discussed the basics of control containment using a frame window as a control container. An even more common place to contain controls is the ever- popular dialog. For quite a while, the Visual C++ resource editor has allowed a control to be inserted into a dialog resource by right-clicking on a dialog resource and choosing Insert ActiveX Control. As of Visual C+ + 6.0, ATL supports creating dialogs that host the controls inserted into dialog resources. The Insert ActiveX Control dialog is shown in Figure 11.3. 574 ATL INTERNALS In^eil Ac:tiv*iX fooliol comor t i n ovieControl Object ActiveS etup. T extCtl Object AcbveXPlugin Object adbanner Class Adofn Control A p p s Control AppWizard6. S ubWizard AxB rowse Ax B rowser Figure 11.3. Insert ActiveX Control dialog The container example provided as part of this chapter has a simple dialog box with a BullsEye control inserted, along with a couple of static controls and a but­ ton. This is what that dialog resource looks like in the . rc file: IDO.BULLSEYE DIALOG DISCARDABLE STYLE DS_MODALFRAME 丨 WS_POPUP | ION "BullsEye" 3, "MS Sans Serif BEGIN 0, 0, 342, 238 WS.CAPTION | WS_SY5MENU CAPT] FONT CONTROL LTEXT CTEXT PUSHBUTTON ""• IX.BULL5EYE • "{7DC59CC5-36C0-11D2-AC05- OOAOC9C8ESOO>,,V WS 一 TABSTOP,7,7,269,224 "AScore: ••, IDC_STATIC ,289,7,22,8 "Static",IDCLSCORE,278,18,46,14,SS.CENTERIMAGE I SS_SUNKEN MC1ose”,IDCANCEL,276,41,50,14 Control Initialization Notice that the window text part of the CONTROL resource is a CLSID, specifically, the CLSID of the BullsEye control. This window text will be passed to an instance of the AtlAxWi n window class to determine the type of the control to create. In ad­ dition, another part of the . rc file maintains a separate resource called a DLCINIT resource, which is identified with the same ID as the BullsEye control on the dialog, CONTROL CONTAINMENT 575 that is, IDC_BULLSEYE. This resource contains the persistence information, con­ verted to text format, that will be handed to the BullsEye control at creation time (via IPersi stStream lni t ): IDO.BULLSEVE DLGINIT BEGIN IDC一 BULLSEYE, 0x376, 154, 0 0x0026, 0x0000, 0x007b, 0x0039, 0x0035, 0x0036, 0x0046, 0x0043, 0x0032 • • • 0x0000, 0 0x0040 , 0x0000, 0x0020, 0x0000, END Because most folks prefer not to enter this information directly, right-clicking on a COM control and choosing Properties will show the control’s property pages, along with the custom property pages of the resource editor. Figure 11.4 shows the Bulls­ Eye properties dialog. • The DLGINIT resource for each control is constructed by asking each control for IP ersi stStream lni t, calling Save, converting the result to a text format, and dumping it into the . rc file. In this way, all information set at design time will be automatically restored at runtime. Figure 11.4. BullsEye properties dialog 576 ATL INTERNALS Sinking Control Events in a Dialog Recall that sinking control events requires adding one ID i spEventlmpl per con­ trol to the list of base classes of your dialog class and populating the sink map. Al­ though this has to be done by hand if a window is the container, it can be performed automatically if a dialog is to be the container. By right-clicking on the control and choosing Events, you can choose the events to handle, and the IDi spEventlmpl and sink map entries will be added for you. Figure 11.5 shows the Event Handlers dialog. However, although the wizard will add the ID i spEventlmpl classes and manage the sink map, it will not insert code to call AtlAdviseSinkMap, either in WM_INITDIALOC to establish connection points with the controls or in WM— DESTROY to tear down the connection points. You have to remember to do this yourself. Figure 11.5. BullsEye Event Handlers dialog CONTROL CONTAINMENT 577 CAxDialoglmpI In Chapter 9,I discussed the CDi al oglmpl class, which, unfortunately, is not able to host controls. Recall that the member function wrappers DoModal and Create merely call the Win32 functions DialogBoxParam and CreateDialogParam. Since the built-in dialog box manager window class has no idea how to host con­ trols, ATL has to perform some magic on the dialog resource. Specifically, it must preprocess the dialog box resource looking for CONTROL entries and replacing them with entries that will create an instance of an AtlAxWi n window. Once this is done, the AtlAxWi n uses the name of the window to create the control and the DLGINIT data to initialize it, providing all the control hosting functionality we’ve spend most of this chapter dissecting. To hook up this preprocessing step when hosting con­ trols in dialogs, we use the CAxDialoglmpl base class: template class ATL_NO—VTABLE CAxOialoglmpl : public CDialoglmplBaseT< TBase >{ public: II modal dialogs int OoModal(HWNO hWndParent * ::CetActiveWindow(), LPARAM dwlnitParam « NULL) { _Module.AddCreateWndOata(&m_thunk.cd, (CDi aloglmplBaseT< TBase >*)this); return AtlAxDialog8ox(_>todu1e.GetResourceInstance(), MAKEINTRESOURCE(T::IDO), HWndParent. (DLCPROC)T::StartDi alogProc, dwlnitParam); } BOOL EndDia1og(int nRetCode) { return ::EndDialog(m_hWnd, nRetCode); } II modeless dialogs HWNO Create(HWND hWndParent, LPARAM dwlnitParam * NULL) { _Module.AddCreateWndDataC&wuthunk.cd, (COi aloglmplBaseT< TBase >*)this); HWND HWnd « AtlAxCreateOialog(_Modu1e.CetResourceInstance(), MAKEINTRESOURCE(T::I00), hWndParent, (DLGPROC)T::Start0ia1ogProc, dwlnitParam); return hWnd; } 578 ATL INTERNALS // for CComControl HWNO Create(HWND HWndParent, RECTA, LPARAM dwlnitParam ■ NULL) { return Create(hWndParent, dwlnitParam); } BOOL OestroyW1ndow() { return ::DestroyWindow(m_hWnd); } Notice that the DoModal and Create wrapper functions call AtlAxDialogBox and AtlAxCreateDi al og instead of Di a l ogBoxParam and CreateDi al ogParam, respectively. These functions perform the preprocessing necessary to create an in­ stance of AtlAxWi n window class for each CONTROL entry in the dialog resource. Using CAxDi a l oglm pl as the base class, we can have a dialog that hosts COM controls like so: class CBullsEyeDIg : public CAxDialoglmplf public IDispEventlnpl { public: BECIN_MSC_MAP(CBul1sEyeDlg) MESSACE_HANDLER(VW_DESTROY, OnDestroy) MESSACE_HANDLER(WM_INITDIALOG, OnlnitDialog) COMMAND_IO_HANDLER(IDCANCEL, OnCancel) END_>ISC_MAP() BECIN_SINK_MAP(CBul1sEyeOlg) SINK_ENTRY(IDCLBULLSEYE, 0x2, OnScoreChanged) END_SINK_MAP() // Map this cl as to a specific dialog resource enum { IDD » IDO—BULLSEYE }; // Hook up connection points LRESULT OnlnltDialogC...) { AtlAdviseS1nkMapCth1s, true〉; return 0; } // Tear down connection points LRESULT OnDestroy(UINT uMsg, WPARAM MParaat LPARAM 1Para«, BOOLA bHandl ed) { AtlAdviscSinkMapCthis, false); return 0; > CONTROL CONTAINMENT 579 // Window control event handlers LRESULT OnCancelOORD, UINT, HWND, B00L&); II COM control event handlers VOID 一 stdcal1 OnScoreChanged(LONG ringValue); }; Notice that, just like a normal dialog, the message map handles messages for the dialog itself (such as WM_INITDIALOG and WMJDESTROY) as well as provides a mapping between the class and the dialog resource ID (via the IDD symbol). The only thing new is that, because we’ve used CAxDi a l oglm pl as the base class, the COM controls will be created as the dialog is created. Attaching a CAxWindow During the life of the dialog, you will likely need to program against the interfaces of the contained COM controls, which means you'll need some way to obtain an in- terface on a specific control. One way to do this is with an instance of CAxWi ndow. Since ATL has created an instance of the A tl AxWi n window class for each of the COM controls on the dialog, you use the A ttach member function of a CAxWi ndow to attach to a COM control, and thereafter use the CAxWi ndow object to manipulate the host window. This is very much like you’d use the A tta c h member function of the window wrapper classes discussed ir\ Chapter 9 to manipulate an edit control. Once you’ve attached a CAxWi ndow object to an A tl AxWi n window, you can use the member functions of CAxWi ndow to communicate with the control host window. Recall the QueryControl member function to obtain an interface from a control, as shown here: class CBullsEyeDIg : public CAxDialoglmpl, public IDiSpEventlmpI { public: LRESULT OnlnitDialogC.•.) { II Attach to the BullsEye control m_axBunsEye.Attach(GetDlgItemCIDC_BULLSEYE)); I I Cache BullsEye interface m_axBul1sEye.QueryControl(&m_spBullsEye); } return 0; 580 ATL INTERNALS private: CAxWindow «_axBullsEye; CComFtr __spBullsEye; }; In this example, I’ve cached both the HWND to the At IAxWi n, for continued com­ munication with the control host window, and one of the control’s interfaces, for communication with the control itself. If you don’t need the HWND, but only an in­ terface, you may want to consider using GetDl gControl instead. GetDlgControl The CDi a l oglm pl class, because it derives from CWi ndow, provides the GetDl g- Item function to retrieve the HWND of a child window given the ID of the child. Like­ wise, CWi ndow also provides a CetDl gControl member function, but to retrieve an interface pointer instead of an HWND: HRESULT CWi ndow::CetDlgControl(int nID, REFIIO iid, void** ppUnk) { HRESULT hr « E^FAIL; HWND hWndCtrl * CetDlgltem(nlO); if (hWndCtrl U NULL) { •ppUnk « NULL; CComPtr spUnk; hr • AtlAxCetControl(hWndCtrl, AspUnk); if (SUCCEEDEO(hr)) hr * spUnk->QueryInterface(iid, ppUnk); } return hr; > The GetDl gControl member function calls the AtlAxCetControl function, which uses the HWND of the child window to retrieve an IUnknown interface. A tl Ax- C etC ontrol does this by sending the WM_GETC0NTR0L window message that win­ dows of the class A tl AxWi n understand. In the event that the child window is not an instance of the A tl AxWi n window class, or if the control does not support the inter­ face being requested, GetDl gC ontrol will return a failed HRESULT. Using CetDl g- C o n tro l simplifies the code to cache an interface on a control considerably: LRESULT OnlnitDia1og(...) { // Cache BullsEye interface GetDl gControl CIDC_BULLSEYE, IID^IBul IsEye, (void**)&m_spBiillsEye); ■ •參 return 0; } CONTROL CONTAINMENT 581 The combination of the CAxDi al oglmpl class, the control containment wizards in Visual C++, and the GetDlgControl member function makes managing COM controls in a dialog much like managing Windows controls. Composite Controls Declarative User Interfaces for Controls There’s beauty in using a dialog resource for managing the user interface (UI) of a window. Instead of writing pages of code to create, initialize, and place controls on a rectangle of gray, we can use the resource editor to do it for us. At design time we lay out the size and location of the elements of the UI, and the ATL-augmentcd dia­ log manager is responsible for the heavy lifting. This is an extremely useful mode of UI development, and it’s not limited to dialogs. It can also be used for composite controls. A composite control is a COM control that uses a dialog resource to lay out its UI elements. These UI elements can be Windows controls or other COM controls. To a Windows control, a composite control appears as a parent window. To a COM control, the composite control appears as a control container. To a control container, the composite control appears as a control itself. To the developer of the control, a composite control is all three. In fact, take all the programming tech­ niques from Chapter 10, combine them with the techniques I ,ve shown you thus far in this chapter, and you have a composite control. CComCompositeControl ATL provides support for composite controls via the CComComposi teControl base class: template class CComComposi teControl : public CCo«G>ntro1< T, CAxDialoglmpI< T > > Notice that CComComposi teC ontrol derives both from CComControl and CAx- Di al oglmpl, combining the functionality of a control and the drawing of the dia­ log manager, augmented with the COM control hosting capabilities of AtlAxWi n. Both of the Wizard-generated composite control types (composite control and lite composite control) derive from CComComposi teC ontrol instead of CComCon­ tr o l and provide an IDD symbol mapping to the control’s dialog resource: class ATL_NO^VTABLE CDartBoard : public CComObjectRootEx 582 ATL INTERNALS public CComCompositeControl, 攀參籲 public CComCoClass { public: • • • enua { IOD * IOOJ)ARTBOARD }; CDartBoardO { // Composites can't be windowless iiL.bWindowOnly - TRUE; II Calculate natural extent based on dialog resource size CalcExtent(nusi zeExtent); } Notice that the construction of the composite control sets the m_bWi ndowOnl y flag, disabling windowless operation. The control’s window needs to be of the same class as that managed by the dialog manager. Also notice that the m_si zeExtent member variable is set by a call to CalcExtent, a helper function provided in CComComposi teC ontrol. CalcExtent is used to set the initial preferred size of the control to be exactly that of the dialog box resource. Composlte-Control Drawing Since a composite control is based on a dialog resource, and its drawing will be managed by the dialog manager and the child controls, no real drawing chores have to be performed. Instead, setting the state of the child controls, which will cause them to redraw, is all that’s required to update the visual state of a control. For example, the DartBoard example available with the source code of this book uses a dialog resource to lay out its elements, as shown in Figure 11.6. This dia­ log resource holds a BullsEye control, two static controls, and a button. When the user clicks on a ring of the target, the score is incremented. When the Reset button is pressed, the score is cleared. The composite control takes care of all the logic, but the dialog manager performs the drawing. However, in the case in which a composite control is shown but not activated, the composite control’s window will not be created and the drawing must be done manually. For example, a composite control must perform its own drawing when hosted in a dialog resource during the design mode of the Visual C++ resource CONTROL CONTAINMENT 583 Figure 11.6. DartBoard composite control dialog resource editor. The ATL Object Wizard will generate an implementation of OnDraw that handles this case, as shown in Figure 11.7.1 find this implementation somewhat in­ convenient, because it doesn’t show the dialog resource as I’m using the control. Specifically, it doesn’t show the size of the resource. Toward that end, I’ve provided another implementation that is a bit more helpful, as shown in Figure 11.8. This implementation shows the light gray area as the recommended size of the control based on the current dialog resource. The dark gray area is the part of the control that is still managed by the control, but is outside the area managed by the dialog manager. The updated OnD「aw implementation is shown here: // Draw an inactive composite control vi rtual HRESULT OnDraw(ATL_DRAWINFO& di) { ifC m.blnPlaceActive ) return S一 OK; II Draw background rectangle S e le c tO b je c t(d i. hdcDraw, GetStockObject(BLACK_PEN)); SelectObject(di.hdcDraw, CetStockObj ect(GRAY_BRUSH)); Rectang1e(di.hdcDraw, di.prcBounds->left, di,prcBounds->top, di.prcBounds->right, di.prcBounds->bottom); Figure 11.7. Default CComCom- posi teControl’s inactive OnDraw implementation 584 ATL INTERNALS Figure 11.8. Updated CComComposi te C o n tro l’s inactive OnDraw implementation II Draw proposed dialog rectangle SIZE sizeMetric; CalcExtent(sizeMetric); SIZE sizeDialog; AtlHi Met ri cToPi xel(&si zeMetri c, &sizeDia1og); SIZE sizeBounds * { di.prcBounds->right-di.prcBounds->left, di.prcBounds->bottom-di.prcBounds->top }; SIZE sizeOialogBounds « { min(sizeOialog.cx, sizeBounds.cx), min(sizeDialog.cy, sizeBounds.cy) >; RECT rectDialogBounds = { di.prcBounds->left, di.prcBounds->top, di.prcBounds->left + si zeOi alogBounds.cx, di.prcBounds->top + si zeDi alogBounds.cy } SelectObject(di.hdcDraw, CetStockObj ect(LTCRAY.BRUSH)); Rectangle(di•hdcDraw, rectDialogBounds•left, rectDialogBounds•top, rectDi alogBounds.right, rectDialogBounds.bottom); II Report natural and current size of dialog resource SetTextColor(di.hdcDraw, ::CetSysColor(COLOR_WINDOVTEXT)); SetBkMode(di .hdcDraw, TRANSPARENT)*; CONTROL CONTAINMENT TCHAR sz[256]; wsprintf(szf 一 T("Recommended: %d x %d\r\nCurrent: %d x %d"), sizeDialog.cx, sizeDialog.cy, sizeBounds.cx, sizeBounds.cy); DrawText(di.hdcDraw, sz, -1, &rectDialogBounds, OT_CENTER); return S_OK; } Using a dialog resource and deriving from CComComposi teC ontrol are the only differences between a control that manages its own UI elements and one that leans on the dialog manager. A composite control is a powerful way to lay out a con- trol’s UI elements at design time. However, if you really want to wield the full power of a declarative UI when building a control, you need an HTML control. HTML Controls Generating an HTML Control The HTML control is available from the ATL Object Wizard in the Controls category as either an HTML Control or a Lite HTML Control. The wizard-generated code will create a control that derives from CComControl, sets m_bWindowOnly to TRUE, and provides a resource for the layout of the UI elements of your control. This is very similar to the resource resulting from running the Object Wizard for a com­ posite control, except that instead of using a dialog resource, an HTML control will use an HTML resource. The same WebBrowser control that provides the UI for In­ ternet Explorer 4.0 十 will provide the parsing for the HTML resource at runtime. This allows a control to use a declarative style of UI development, but with all the capabilities of the HTML engine in Internet Explorer. The following are a few of the advantages that HTML provides over a dialog resource: ■ Support for resizing via height and wi dth attributes, both in absolute pixels and percentages. _ Support for scripting when using top-level initialization code, defining func­ tions, and handling events. ■ Support, for extending the object model of the HTML document via an “exter- naT object. ■ Support for flowing of mixed text and graphics. ■ Support for multiple font families, colors, sizes, and styles. ATL INTERNALS In fact, pretty much everything you’ve ever seen on a web site can be performed us­ ing the HTML control. HTML Control Creation The magic of hooking up the WebBrowser control is performed in the OnCreate handler generated by the ATL Object Wizard: LRESULT CSmartOartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) { // Wrap the control's window to use it to host control // (not an AtlAxWin, so no CAxHostWindow yet created) CAxWi ndow wnd(n_hWnd); // Create a CAxWinHost: It will subclass this window and // create a control based on an HTHL resource. HRESULT hr - wnd.CreateControl CIDH3MARTDARTB0ARD); • • • return SUCCEEDEO(hr) ? 0 : -1; > Because nubWi ndowOnly is set to tru e , activation of the HTML control will cause a window to be created. To give this window control containment capabili­ ties so that it may host an instance of the WebBrowser control, the HTML contrors window must be subclassed and sent through the message m即 provided by CAx­ HostWi ndow, just like every other co