2008-11-02
[转]5分钟看明白美国金融危机爆发的原因!
二。 CDS合同。由于杠杆操作高风险,所以按照正常的规定,银行不运行进行这样的冒险操作。所以就有人想出一个办法,把杠杆投资拿去做“保险”。这种保险就叫 CDS。比如,银行A为了逃避杠杆风险就找到了机构B。机构B可能是另一家银行,也可能是保险公司,诸如此类。A对B说,你帮我的贷款做违约保险怎么样, 我每年付你保险费5千万,连续10年,总共5亿,假如我的投资没有违约,那么这笔保险费你就白拿了,假如违约,你要为我赔偿。A想,如果不违约,我可以赚 45亿,这里面拿出5亿用来做保险,我还能净赚40亿。如果有违约,反正有保险来赔。所以对A而言这是一笔只赚不赔的生意。B是一个精明的人,没有立即答 应A的邀请,而是回去做了一个统计分析,发现违约的情况不到1%。如果做一百家的生意,总计可以拿到500亿的保险金,如果其中一家违约,赔偿额最多不过 50亿,即使两家违约,还能赚400亿。A,B双方都认为这笔买卖对自己有利,因此立即拍板成交,皆大欢喜。
三。 CDS市场。B做了这笔保险生意之后,C在旁边眼红了。C就跑到B那边说,你把这100个CDS卖给我怎么样,每个合同给你2亿,总共200亿。B想,我 的400亿要10年才能拿到,现在一转手就有200亿,而且没有风险,何乐而不为,因此B和C马上就成交了。这样一来,CDS就像股票一样流到了金融市场 之上,可以交易和买卖。实际上C拿到这批CDS之后,并不想等上10年再收取200亿,而是把它挂牌出售,标价220亿;D看到这个产品,算了一 下,400亿减去220亿,还有180亿可赚,这是“原始股”,不算贵,立即买了下来。一转手,C赚了20亿。从此以后,这些CDS就在市场上反复的抄, 现在CDS的市场总值已经抄到了62万亿美元。
四。 次贷。上面 A,B,C,D,E,F....都在赚大钱,那么这些钱到底从那里冒出来的呢?从根本上说,这些钱来自A以及同A相仿的投资人的盈利。而他们的盈利大半来 自美国的次级贷款。人们说次贷危机是由于把钱借给了穷人。笔者对这个说法不以为然。笔者以为,次贷主要是给了普通的美国房产投资人。这些人的经济实力本来 只够买自己的一套住房,但是看到房价快速上涨,动起了房产投机的主意。他们把自己的房子抵押出去,贷款买投资房。这类贷款利息要在8%-9%以上,凭他们 自己的收入很难对付,不过他们可以继续把房子抵押给银行,借钱付利息,空手套白狼。此时A很高兴,他的投资在为他赚钱;B也很高兴,市场违约率很低,保险 生意可以继续做;后面的C,D,E,F等等都跟着赚钱。
五。次贷危机。房价涨到一定的程度就涨不上去了,后面没人 接盘。此时房产投机人急得像热锅上的蚂蚁。房子卖不出去,高额利息要不停的付,终于到了走头无路的一天,把房子甩给了银行。此时违约就发生了。此时A感到 一丝遗憾,大钱赚不着了,不过也亏不到那里,反正有B做保险。B也不担心,反正保险已经卖给了C。那么现在这份CDS保险在那里呢,在G手里。G刚从F手 里花了300亿买下了100个CDS,还没来得及转手,突然接到消息,这批CDS被降级,其中有20个违约,大大超出原先估计的1%到2%的违约率。每个 违约要支付50亿的保险金,总共支出达1000亿。加上300亿CDS收购费,G的亏损总计达1300亿。虽然G是全美排行前10名的大机构,也经不起如 此巨大的亏损。因此G濒临倒闭。
六。金融危机。如果G倒闭,那么A花费5亿美元买的保险就泡了汤,更糟糕的是,由 于A采用了杠杆原理投资,根据前面的分析,A赔光全部资产也不够还债。因此A立即面临破产的危险。除了A之外,还有A2,A3,...,A20,统统要准 备倒闭。因此G,A,A2,...,A20一起来到美国财政部长面前,一把鼻涕一把眼泪地游说,G万万不能倒闭,它一倒闭大家都完了。财政部长心一软,就 把G给国有化了,此后A,...,A20的保险金总计1000亿美元全部由美国纳税人支付。
七。美元危机。上面讲 到的100个CDS的市场价是300亿。而CDS市场总值是62万亿,假设其中有10%的违约,那么就有6万亿的违约CDS。这个数字是300亿的200 倍。如果说美国政府收购价值300亿的CDS之后要赔出1000亿。那么对于剩下的那些违约CDS,美国政府就要赔出20万亿。如果不赔,就要看着 A20,A21,A22等等一个接一个倒闭。无论采取什么措施,美元大贬值已经不可避免。
以上计算所用的假设和数字同实际情况会有出入,但美国金融危机的严重性无法低估。
Read more ...
2008-10-30
卖猪男如何引发了经济危机(转贴)
少妇说:家里只一人不便。
男:求你了大妹子,给猪一头。
女:好吧,但家只有一床。
男:我也到床上睡,再给猪一头。
女:同意。
半夜男与女商量,我到你上面睡,女不肯。
男:给猪两头。
女允,要求上去不能动。
少顷,男忍不住,央求动一下,女不肯。
男:动一下给猪两头。女同意。
男动了八次停下,女问为何不动?
男说猪没了。
女小声说:要不我给你猪……
天亮后,男吹着口哨赶30头(含少妇家的10头)猪赶集去了……
哈佛导师评论:要发现用户潜在需求,前期必须引导,培养用户需求,因此产生的投入是符合发展规律的。
————————————
(加强篇)
另一男得知此事,决意如法炮制,遂赶集卖猪,天黑遇雨,二十头猪未卖成,到一农家借宿
少妇说:家里只一人不便。
男:求你了大妹子,给猪一头
女:好吧,但家只有一床。
男:我也到床上睡,再给猪一头。
女:同意。
半夜男商女,我到你上面睡,女不肯。
男:给猪两头。
女允,要求上去不能动。
少顷,男忍不住,央求动一下,女不肯。
男:动一下给猪两头。女同意。
男动了七次停下,女问为何不动?
男说:完事了~~~女:......
天亮后,男低著头赶2头猪赶集去了......
哈佛导师评论:要结合企业自身规模进行谨慎投资,谨防资金链断裂问题
————————————
又一男得知此事,决意如法炮制兼吸取教训,遂先用一头猪去换一粒伟哥,事必,天亮后,男吹着口哨赶38头(含少妇家的18头)猪赶集去了……
哈佛导师评论:企业如果获得金融资本的帮助,自身经营能力将得到倍增。
————————————
知道此法男多,伟哥供不应求,逐渐要2头,3头猪换一粒伟哥。
哈佛导师评论:这就是通货膨胀。
————————————
当猪价格涨到16粒一棵的时候,哈佛导师评论:该男已经进入边际成本,除了拥有对自身能力的自信和未来良好愿望以外,实际现猪流已经为零。
但换猪男越来越多,卖伟哥的决定,扩展生产能力,推出一种次级伟哥,如果你缺一头猪,只要你承诺可以到该女房中一夜,就可以先借,事成后补交猪款,这个方法大大促进了伟哥销售。
哈佛导师评论:这就是贷款,让企业可以根据未来的收益选择借支流动资金
————————————
伟哥专卖店后来在即使你一头猪都没有,只要你承诺可以到该女房中一夜,就可以先借,事成后补交猪款。
哈佛导师评论:这就是金融创新,让现在的人花未来的钱,反正等你老了未来的钱你也花不动。
————————————
消息一出,换猪男越来越多,有人找伟哥专卖店,这个项目太好了,我们把它变成优质基金,对外销售债卷,你们也就可以分享我的收益,如何?
结果伟哥专卖店觉得甚好,于是该公司把换猪男分三类,一类是拿现猪换的,一类是一部分现猪贷的,一类是完全没有现猪借的,发行三种债卷。大家踊跃而上。纷纷购买伟哥专卖店的债卷,伟哥专卖店生意太好,就把债卷销售外包给另外一家公司运作,该公司也一并大发其财,公司越做越大,甚至可以脱离实际伟哥销售情况来发行,给自己和伟哥专卖店带来巨大的现金收益。
哈佛导师评论:这就是专业的人做专业的事,从实体经营到资本运作,经济进入了更高的层次。
————————————
为了防止自己债卷未来有损失,该公司决定给它买上保险,这样债卷销售就更容易,因为一旦债卷出现问题,还可以获得保险公司的赔付,哇,债券公司销售这下子太好了,保险公司也获得巨大平白无故的保险收入。
哈佛导师评论:这就是风险对冲,策略联盟,提高了企业的抗风险能力,也保护了消费者利益。
————————————
换猪男太多,排长队等待,该女无法承受,说老娘不干了,我搬家,一时间有无数拥有伟哥的欠猪男。
哈佛导师评论:这是个别现象,属于市场的正常波动,不会影响整个经济。
————————————
结果该女迟迟不肯搬回。一部分欠猪男没有收入,只好赖帐,结果大量债卷到期无法换现猪吃,债卷公司一看,一粒伟哥16头猪,这哪里还得起,宣布倒闭
哈佛导师评论:这是次贷危机,不会影响整个金融行业。
————————————
哪里晓得债卷公司还把债卷上了保险,保险公司一看,这哪里赔得起,于是也宣布要倒闭。
哈佛导师评论:这是金融危机,还不会影响整个实体经济。
————————————
后文:据说该女已搬到中国定居。
Read more ...
2008-10-17
为何法电视台转向
2008-10-06
Netbeans 的字体、默认语言和默认编码
- 找到netbeans所用JDK的文件夹,例如:D:\Java\jdk\jdk1.6.0_07\jre\lib
- 复制文件fontconfig.properties.src为fontconfig.properties
- 修改fontconfig.properties, 将其中的sequence.monospaced.***中的内容换个顺序,也就是将alphabetic放到Chinese-***之前。
- 保存后,运行netbeans
修改netbeans的默认语言和编码
netbeans的默认配置文件:
C:\Program Files\NetBeans 6.0.1\etc\netbeans.conf
主要的修改就是 netbeans_default_options 选项,
对于我而言,具体有用的参数有:
a. --locale en_US
将locale定义为英语,则启动界面就是英文的了;
b. -J-Dfile.encoding=UTF-8
这个是java的选项,定义文件的默认编码为utf8;
c. --laf javax.swing.plaf.metal.MetalLookAndFeel Read more ...
施明德谈马英九
这基本价值,你要追求什么样的价值,我想在他一生当中,你在追求这些价值的时候,如果一个敢坚持价值的人,他不忌讳异议,他遇山开路,遇河架桥,遇到危险挺住它,遇到压力我就面对它,但是他经常很在意媒体。你看媒体反对的时候,他马上变来变去,马上往这边、又这边。
施明德:我想,我不想来评论蔡英文,我对一个“政党”或者一个“政治领袖”,我总有几个标准是我恒久以来认为的。这个“政党”它有没有引领大方向的智慧,应该告诉我们大方向在哪里?“政治领袖”你要具备这样的大方向的智慧,“政党”你要说出你要把人民带到哪一个方向去。第二个坚守大原则的气节,就是压力来你能不能扛得住,诱惑来的时候你能不能拒绝。然后第三个,你用人,这我是认为是非常重要的。第四个你要鼓舞人民的信心,不管是谁,人都有好坏,“国家”有好坏,好的时候你怎么让它能够胜不骄,坏的时候你怎么鼓舞士气,这个是很重要的。好,我讲民进党到今天,我一直看不出来,看不出来!
我认为,虽然他谈的是马英九和民进党,但所言对于做人,做事业同样适用。 Read more ...
Using blogger as a project menagement platform
We know Blogger.com are amazing blogger platform to write blog, share with friends and interact with friends. Thanks Blogger.com for providing us the powerful, totally free platform. Then, how to manage project throght blogger.com? Let's figure it out!
Commonly, the project management should have two major function:
- project wiki
- project issue management
Other people in the project team could view all the project information through these labels, and can comment on it. So this is wiki feature.
The issue management is also a blog article. We create a blog with label "Bug" as an issue. when the issue has been fixed, we label this blog "Fixed". when it has been reopen, we fixed it with "reopen" and so on.
Thus, we using label to manange project wiki and issues. Threre are also some skills to help us using blog as a project menagement platform:
- prject name can be a label.
- if the project are private, you can manage who can read your blog by blogger settings.
- if you have many projects and every project has its different authority strategy, you can create a blog site for every project.
By using blogger as a project menagement platform, there are some benefits:
- free to use
- fully extends to many projects
- more tricks of blogger you can use
2008-05-07
Delphi2007 Vista 下的COM+调试

"C:\Windows\system32\vsjitdebugger.exe" C:\Windows\system32\dllhost.exe /ProcessID:{1CE65B89-9CED-4BB2-A514-A54DE7A5B0A5}
备注:Delphi7 Just in time debug: "...Delphi7\bin\bordbg70.exe" -aeargs %ld %ld Read more ...
2008-04-30
电力专业英文单词翻译
发电机 generator
励磁 excitation
励磁器 excitor
电压 voltage
电流 current
升压变压器 step-up transformer
母线 bus
变压器 transformer
空载损耗 no-load loss
铁损 iron loss
铜损 copper loss
空载电流 no-load current
有功损耗 active loss
无功损耗reactive loss
输电系统 power transmission system
高压侧 high side
输电线 transmission line
高压 high voltage
低压 low voltage
中压 middle voltage
功角稳定 angle stability
稳定 stability
电压稳定 voltage stability
暂态稳定 transient stability
电厂 power plant
能量输送 power transfer
交流 AC
直流 DC
电网 power system
落点 drop point
开关站 switch station
调节 regulation
高抗 high voltage shunt reactor
并列的 apposable
裕度 margin
故障 fault"
电流继电器
current relay
电压继电器
voltage relay
跳闸继电器
tripping relay
合闸继电器
closing relay
中间继电器
intermediate relay
时间继电器
time relay
零序电压继电器
zero-sequence voltage relay
差动继电器
differential relay
闭锁装置
locking device
遥控
telecontrol
遥信
telesignalisation
遥测
telemetering
遥调
teleregulation
断路器
breaker,circuit breaker
少油断路器
mini-oil breaker,oil-mini-mum breaker
高频滤波器
high-frequency filter
组合滤波器
combined filter
常开触点
normally opened contaact
常闭触点
normally closed contaact
并联电容
parallel capacitance
保护接地
protective earthing
熔断器
cutout,fusible cutout
电缆
cable
跳闸脉冲
tripping pulse
合闸脉冲
closing pulse
一次电压
primary voltage
二次电压
secondary voltage
并联电容器
parallel capacitor
无功补偿器
reactive power compensation device
消弧线圈
arc-suppressing coil
母线
Bus,busbar
三角接法
delta connection
星形接法
Wye connection
原理图
schematic diagram
一次系统图
primary system diagram
二次系统图
secondary system diagram
两相短路
two-phase short circuit
三相短路
three-phase short circuit
单相接地短路
single-phase ground short circuit
短路电流计算
calculation of short circuit current
自动重合闸
automatic reclosing
高频保护
high-freqency protection
距离保护
distance protection
横差保护
transverse differential protection
纵差保护
longitudinal differential protection
线路保护
line protection
过电压保护
over-voltage protection
母差保护
bus differential protection
瓦斯保护
Buchholtz protection
变压器保护
transformer protection
电动机保护
motor protection
远方控制
remote control
用电量
power consumption
载波
carrier
故障
fault
选择性
selectivity
速动性
speed
灵敏性
sensitivity
可靠性
reliability
电磁型继电器
electromagnetic
无时限电流速断保护
instantaneously over-current protection
跳闸线圈
trip coil
工作线圈
operating coil
制动线圈
retraint coil
主保护
main protection
后备保护
back-up protection
定时限过电流保护
definite time over-current protection
三段式电流保护
the current protection with three stages
反时限过电流保护
inverse time over-current protection
方向性电流保护
the directional current protection
零序电流保护
zero-sequence current protection
阻抗
impedance
微机保护
Microprocessor Protection
2008-04-26
DLL 應用 - 設計可抽換的模組
DLL 應用 - 設計可抽換的模組
作者:蔡煥麟
日期:Jan-2-2001
摘要:介紹以 DLL 來切割應用程式的實作方式,其中包含介面程式設計的技巧以及運用 Design Patterns 來解決設計上的問題。
前言
DLL(Dynamic Link Library,動態聯結函式庫)就目前來講已經不是什麼了不得的技術,坊間書籍隨手撿一本視窗程式設計或 Delphi 的書籍都可以找到 DLL 的相關說明,少這一篇也不算少,之所以寫這篇文章,一方面是給自己的學習心得作個記錄,一方面也提供給有需要的人參考;而本文的主題--設計動態載入的模組--說穿了也只是提供一個把 Form 包在 DLL 裡面的實作方法,儘管如此,我還是希望你能在其中發現一些比較不一樣的東西。
由於現有關於 DLL 的文件資料已經很多,在此不多做重複,因此在閱讀本文時會需要一些 DLL 的基礎知識或者 DLL 的撰寫經驗,這樣閱讀起來會比較輕鬆。以下就重點式地列出一些基礎觀念:
- 靜態連結與動態連結的差異。
- 了解如何宣告 DLL 輸出函式(exported functions)以及如何在外部呼叫它們。
- 各種呼叫慣例(calling conventions)的差異。
- 何謂 DLL hell(1)以及它對應用程式的維護有何影響。
DLL 在使用上又有靜態與動態載入的區別,所謂「靜態載入的 DLL」意指在編譯時期已經確定要連結的 DLL 是哪一個,而且會在行程初始化的階段就被載入,Delphi 的 VCL packages 即屬此類。動態載入的 DLL 則是執行時期需要時才載入,在程式撰寫上比靜態載入的方式麻煩些,但較有彈性,且應用程式啟動的速度也較快。
本文所要討論的就是以動態載入的 DLL 來實作可抽換的應用程式模組。
以 DLL 切割應用程式
一般來說,使用 DLL 有下列優點:
- 節省記憶體。多個應用程式使用同一個 DLL 時,該 DLL 只會被載入一次,甚至可以用到時才載入 DLL,且用完立即釋放。
- 程式碼重複使用,可讓不同的程式語言使用。
- 應用程式模組化,可降低應用程式的複雜度,程式更新維護時較方便。
- 可支援設計多國語言的應用程式。你可以把每一種語言的字串資源分別存放在一個 DLL 裡面,程式執行時便可以動態切換程式所使用的語言。
但也會一些困難必須克服,當我們要將應用程式切割成數個 DLL 模組的時候,通常會碰到以下幾個問題:
- DLL 如何輸出(export) VCL 物件?
- 如何將一個 Form 包在 DLL 裡面以供外部使用?
- DLL 之間如何共享變數?
基本上,如果你撰寫成 package 的形式就沒有上述問題了,但你可能會遇到其他麻煩,例如:名稱衝突的問題,這包括了型態、單元名稱、函式名稱的衝突,在此之前我也曾試著以 package 的方式來撰寫可抽換的模組,但名稱衝突的問題令我覺得蠻困擾。我也曾在另一份文件中提及此事,以下這段文字是從該文轉貼上來的(2):
「在撰寫幾個 package 的測試程式之後,我還是沒有將 package 應用在實際的專案開發中,而仍然使用 DLL,其最主要的原因,正是 package 優於 DLL 之處--可以共享變數。這項功能的立意很好,但也帶來了另一些限制,主要是名稱衝突的問題,使得共用的 unit 一定要放在 package 裡面,否則當兩個 package 包含了相同的 unit,其中一個就無法載入,我們覺得這會造成麻煩。另外,由於其他的小組成員對於 package 的使用不熟,容易出 trouble(例如:project 要加入 .dcp 之類的),這也是考量之一。」
在 DLL 之間共享變數的問題可以透過記憶體映射檔(memory-mapped file)來解決,你可以在文後所附的參考資料中找到相關資訊,這裡就不贅述。而在 DLL 中輸出 VCL 物件(例如:string)時得注意以下幾點:
- 在 DLL 和其用戶端程式的 Uses 子句裡頭的第一個單元必須是 ShareMem。
- BORLNDMM.DLL 必須跟著你的應用程式一起發佈。
- 如果你修改了輸出物件的類別定義而使得原有物件的記憶體佈局改變,比如說加入一個 Integer 型態的私有成員,用戶端程式就必須重新編譯,如果使用舊的用戶端程式來呼叫新的 DLL 函式,應用程式就會發生錯誤甚至導致當機。
與其隨時注意這些規則,也許選擇可以完全避開這些問題的方法會比較好,我的意思是使用 Windows 的標準型別來傳遞資料,例如要傳遞字串,就用 PChar 來代替 string。對其他較為複雜的結構,可以使用介面來解決,這意味著兩件事情:
- 輸出的型態是個抽象類別(abstract class)或介面(interface)。
- 物件應由 DLL 來建立(用戶端程式不知道物件的記憶體佈局)。
符合了以上的規則,對於如何將 Form 物件包在 DLL 裡面的問題也就迎刃而解,稍後就會講到這部分如何實作。
介 面(interface)在物件導向的領域裡是一個很重要的觀念,它描述了服務提供者和使用者之間的權責,或者說定義了兩個物件之間溝通的方式,通常這個 溝通方式一經制定就不會修改(理想狀況下),因此介面亦可視為物件之間的合約。以 OOP 的角度來看,介面就是一組公開的方法(public methods),跟類別不同之處是它沒有 private 及 protected 等存取等級的區別,不可以包含資料成員,也沒有實作的程式碼,它就只是很單純的....呃...介面。在一個複雜的系統裡面,這種單純顯得特別珍貴,常常 得經過一番深思熟慮之後才能萃取出較為抽象的成分,這不但有助於你在設計時以比較抽象的層次去思考,同時設計出來的介面也比較能夠再拿來重複使用。以用戶 端的角度來看,介面把系統背後的複雜度隱藏了起來,用戶端就只需專注在它需要的部分,使用上會比較容易。 |
設計可抽換的模組
所謂可抽換的模組,就是指在程式執行時動態地載入與釋放的模組,對於規模較龐大,功能較複雜的應用程式來說,將應用程式切割成數個獨立運作的模組有以下優點:
- 應用程式部署的組態更加彈性(例如:有些模組僅包裝於某種版本中)。
- 減少應用程式每次更新版本的檔案大小。
- 有利於明確劃分小組成員的權責。
- 有效地降低單一程式的複雜度,程式較易於維護。
以下會一步步實作出一個具體而微的範例,你可以把這個範例視為一個基礎的框架(framework),稍加修改就可以運用於實際的專案開發上面。
描述需求
讓我們來簡單地分析一下應用程式的需求,假設原本的開發方式是將所有的程式單元編譯連結成一個可執行檔,現在要將應用程式的各個功能切割為獨立的模組,例如:
+--- 客戶資料維護作業(Customer.DLL)
主程式(Main.exe)---+--- 產品資料維護作業(Employee.DLL)
+--- 訂單資料維護作業(Orders.DLL)
其中每個 DLL 都是在使用者執行該項功能的時候才動態載入,而且每個 DLL 裡面至少包含一個 Form,為了有別於一般的 DLL,以下就以 plugin 稱之。我們預期各 plugin DLL 所包含的 Form 會有一些共同的屬性和行為,因此把這些共同點放到一個基礎視窗類別裡面,讓其他 Form 繼承自這個基礎類別。它們的關係看起來像這樣:
每一個維護作業都需要開啟一個視窗,因此主程式的責任之一便是建立並顯示 DLL 裡面的視窗。我們希望每一個維護作業的視窗關閉後才能執行另一個維護作業,所以使用 ShowModal 的方式顯示視窗。做一些簡單的分析之後,可以得到主程式在執行每項作業時所需的共同步驟:
- 載入指定的 plugin DLL。
- 建立並顯示 plugin DLL 裡面的 Form 物件。
- 釋放 Form 物件。
- 釋放 plugin DLL。
其中載入與是釋放 plugin DLL 的工作由主程式負責,而前面有提過 DLL 中的物件必須由 DLL 自己來建立,因此建立、顯示以及釋放 Form 物件的工作都由 plugin DLL 來負責提供函式,主程式只要在適當時機去呼叫它們就行了。
主程式
在主程式中加入一個執行 plugin 的方法,此方法需要一個參數指定 DLL 的檔名以便將其載入執行,像這樣:
procedure TMainForm.RunPlugin(const FileName: string);
var
ADllHandle: THandle;
APlugin: IPlugin;
AFormHandle: THandle;
begin
ADllHandle := SafeLoadLibrary(FileName);
if ADllHandle <> 0 then
begin
APlugin := DllCreatePlugin(ADllHandle, Application.Handle);
try
AFormHandle := APlugin.CreateForm(Handle);
APlugin.ShowModalForm;
APlugin := nil;
FreeLibrary(ADllHandle);
except
FreeLibrary(ADllHandle);
raise;
end;
end
else
ShowMessage('無法載入函式庫: ' + FileName);
end;
從以上程式碼可以看出主程式載入 DLL 之後會呼叫 DllCreatePlugin 來建立 plugin 物件並且取得其介面參考,接著主程式就利用該介面參考來存取 plugin 物件提供的服務,包括建立視窗,顯示視窗等等。很明顯地,IPlugin 介面是主程式和 plugin DLL 之間溝通的橋樑,而且 IPlugin 介面至少要提供下列方法:
CreateForm - 建立 Form 物件
ShowModalForm - 顯示視窗
DestroyForm - 摧毀 Form 物件
眼尖的讀者可能會發現,上面的程式中並沒有呼叫 DestroyForm,而且也沒有呼叫類似 DllDestroyPlugin 的函式來摧毀 plugin 物件,這些物件什麼時候會被釋放掉?
它們是自動被釋放掉的。由於 Form 物件的建立是透過 plugin 物件來完成,所以我打算把摧毀 Form 物件的責任交給 plugin 物件,也就是當 plugin 物件摧毀時會自動將 Form 物件一併釋放掉;而為了簡化摧毀 plugin 物件的動作,我讓 plugin 物件具有自動參考計數的能力,這麼一來只要該物件沒有人使用它(物件的參考計數為 0)就會自動釋放掉了,做法很簡單,只要讓實作 IPlugin 的類別繼承自 TInterfaceObject 就行了,其他細節都由 VCL 幫我們完成了。
LoadLibrary 與 FreeLibrary 也有自己的參考計數,並且用它來決定是否載入及釋放 DLL。也就是說重複呼叫 LoadLibrary('A.DLL') 並不會將 A.DLL 載入兩次,第二次的呼叫只會遞增參考計數而已;同樣的,FreeLibrary 會遞減 DLL 的參考計數,直到計數為 0 才會真正將 DLL 釋放掉。 |
接著看 DllCreatePlugin 函式:
type
TCreatePluginFunc = function (hApp: THandle): IPlugin; stdcall;
const
SDllCreatePluginFuncName = 'CreatePlugin';
implementation
resourcestring
sErrorLoadingDLL = '無法載入模組!';
sErrorDllProc = '無法呼叫 DLL 函式: %s';
function DllCreatePlugin(hLib, hApp: THandle): IPlugin;
var
pProc: TFarProc;
CreatePluginFunc: TCreatePluginFunc;
begin
pProc := GetProcAddress(hLib, PChar(SDllCreatePluginFuncName));
if pProc = nil then
raise Exception.CreateFmt(sErrorDllProc, [SDllCreatePluginFuncName]);
CreatePluginFunc := TCreatePluginFunc(pProc);
Result := CreatePluginFunc(hApp);
end;
DllCreatePlugin 會嘗試從指定的 DLL 模組中呼叫函式 'CreatePlugin' 來建立 plugin 物件,並且傳回 plugin 物件的介面參考,參數 hLib 是 DLL 代碼,而 hApp 則直接傳遞給 DLL 的CreatePlugin 函式,這個參數的作用稍後會解釋。
至此主程式所需的程式碼大致上已經完成了,接下來看看 DLL 的 CreatePlugin 函式。
DLL 的輸出函式
我們的 plugin DLL 只有輸出一個函式供外界呼叫,就是前面提到的 CreatePlugin,其函式原型為:
function CreatePlugin(hApp: THandle): IPlugin; export; stdcall;
CreatePlugin 函式會建立 TPlugin 物件並且傳回 IPlugin 介面的參考。由於 plugin 物件僅需被建立一次,我們可以用一個全域變數實作出簡單的 Singleton(3):
var
g_PluginIntf: IPlugin = nil;
implementation
function CreatePlugin(hApp: THandle): IPlugin;
begin
Application.Handle := hApp; // 讓 EXE 與 DLL 使用同一個 application handle.
if g_PluginIntf = nil then
g_PluginIntf := TPlugin.Create; // TPlugin 的物件參考計數 = 1
Result := g_PluginIntf; // TPlugin 的物件參考計數 = 2
end;
CreatePlugin 需要傳入一個參數 hApp,代表呼叫者程序的 Application 物件的 Handle,通常是傳入 Application.Handle,好讓主程式和 DLL 的 Application 物件能夠「同步」。之所以要這麼做是因為當你的 DLL 專案未使用 "Build with runtime package" 選項時,執行檔和載入的 DLL 會各自有一個 Application 物件,但是只有執行檔的 Application 物件有連結一個視窗,DLL 則沒有,因此 DLL 的 Application.Handle 屬性總是為 0。若少了這個同步的動作,那麼當 DLL 的 Form 開啟時,你會在桌面的工作列上看到多了一個視窗按鈕,看起來就像執行了另一個應用程式一樣,我們不希望看到這種情形。
當然啦,如果你的主程式和 DLL 都使用 "Build with runtime packages" 來建立(你應該這麼做),就不需要這個同步動作了(想想看為什麼?)。
程式碼裡面有兩行關於物件參考計數的註解,是想要表達介面程式設計的一個基本觀念:當一個介面參考在函式之間以 pass by value 的方式傳遞時會遞增物件的參考計數(pass by reference 則不會)。此觀念有助於你正確掌握物件的壽命。
最後別忘了還要把這輸出函式加到專案原始碼的 exports 子句裡頭:
exports
CreatePlugin;
IPlugin 介面與 TPlugin 類別
IPlugin 介面定義如下:
IPlugin = interface
['{D3F4445A-C704-42BC-8283-822541668919}'] // 按 Ctrl+Shift+G 產生 GUID
function CreateForm(hMainForm: THandle): THandle;
procedure DestroyForm;
function ShowModalForm: Integer;
end;
其實以上函式也可以寫成一般的 DLL 函式,當作是 DLL 的介面,之所以另外定義這個介面,一方面是希望簡化 DLL 本身的介面,另一方面也可以集中管理程式碼,以後如果需要增加介面方法的話,只要加在 IPlugin 介面裡面就好了,不用把現有的 DLL 原始碼一個個找出來修改,這也有助於簡化 DLL 的撰寫以及日後的維護工作。
介面不包含實作,實作必須由類別來提供。 |
接著定義一個 TPlugin 類別來實作 IPlugin 介面:
TPlugin = class(TInterfacedObject, IPlugin)
private
FForm: TForm;
public
destructor Destroy; override;
function CreateForm(hMainForm: THandle): THandle;
procedure DestroyForm;
function ShowModalForm: Integer;
end;
在 IPlugin 中加上 GUID 以及讓 TPlugin 繼承自 TInterfacedObject 的目的,是為了讓物件擁有 Interfaced RTTI 以及自動參考計數的能力,這樣我們的 TPlugin 物件就會在沒有任何人使用它時自動釋放掉。私有成員 FForm 記錄了此 plugin 物件所建立的視窗的參考,以便控制其壽命,其型態也可以視需要改成 TBaseForm,那麼你的 TBaseForm 的設計得盡量不要經常修改,或者說設計得抽象一些,讓這些核心的類別在比較抽象的層次上面運作。
各個方法的名稱皆可望文生義,程式碼也很簡單,相信你可以猜個八九不離十,這裡就不一一列出,比較值得一提的是 CreateForm 函式與解構元 Destroy,分述如下:
TPlugin.CreateForm - 使用類別參考來建立物件
在 CreateForm 函式裡面,建立 Form 物件的那行程式是這麼寫的:
FForm := g_ConcreteClass.Create(Application);
其中 g_ConcreteClass 是一個全域變數,其定義為:
var
g_ConcreteClass: TBaseFormClass := nil;
而 TBaseFormClass 是一個類別參考型態(class-reference type),它跟 TBaseForm 定義在同一個單元裡面:
type
TBaseFormClass = class of TBaseForm;
TBaseForm = class(TForm)
.....
end;
也就是說我們用一個類別參考型態的變數 g_ConcreteClass 來記錄欲實體化的類別型態,因此在建立 Form 物件之前還必須先設定 g_ConcreteClass 才行,如此 TPlugin 才能以正確的 Form 類別來進行實體化的動作。
您或許會想為什麼要這麼麻煩,直接寫成像 TCustomerForm.Create 這樣不就好了嗎?
簡單地說,是基於維護的考量。由於在整個 TPlugin 的實作裡面,日後唯一可以能會經常變動的就是要被實體化的 Form 類別,使用類別參考使我們免於在 TPlugin 的實作程式碼裡面把類別型態寫死,以後如果要實體化其他的 Form 類別,只要修改 g_ConcreteClass 這個變數就行了,不用再費一番搜尋及替換文字的功夫,還得擔心有沒有哪裡沒有改到;換句話說,我們等於使用類別參考來讓編譯器幫我們完成這個替換文字的動作,而且保證不會遺漏任何地方。
我交替使用了「建立物件」與「實體化」兩種詞彙,其實它們指的是同一件事情:建立某個類別的實體(instance)。 |
此技巧對於團隊開發也有好處,你只要公佈 TPlugin 和 TBaseForm 兩個單元,然後告訴組員照下面兩個步驟做就行了:
- 從 TBaseForm 衍生一個新類別(可以利用 Delphi 的物件寶庫來簡化這項工作)。
- 在這個新類別的單元的 Uses 子句裡加入 TPlugin 類別所屬的單元,並且在初始化階段把類別名稱指定給 g_ConcreteClass 變數。
在這個範例裡面,我們只有一個 TBaseForm 的後代,叫做 TForm1,因此在 TForm1 的單元裡面會有這一段:
uses
DllExport; // TPlugin 類別實作放在這個單元裡面
.....
initialization
g_ConcreteClass := TForm1;
TPlugin.Destroy
解構函式會呼叫 DestroyForm 使 Form 物件一併釋放掉,並且還原 DLL 的 application handle:
destructor TPlugin.Destroy;
begin
DestroyForm;
Application.Handle := g_DllAppHandle;
inherited Destroy;
end;
其中 g_DllAppHandle 是一個全域變數,其宣告如下:
var
g_DllAppHandle: THandle;
而我們必須在 DLL 初始化的時候將 DLL 本身的 application handle 保存起來:
initialization
g_DllAppHandle := Application.Handle;
其實如果 DLL 專案有用 "Build with runtime package" 選項的話,這個保存及還原 application handle 的動作就可以免了。相反地,若不加上保存及還原的動作,而且 DLL 專案不使用 "Build with runtime package" 選項的話,當 DLL 被釋放時就會發生主視窗也被一併關閉的怪異情形。
擅用原始的力量
到此重要的部分應該都已經提到了,您可能會發現我並沒有對 TBaseForm 多做說明,原因是在這個範例程式中 TBaseForm 並沒有什麼特別之處,只是為日後擴充時預留的一個基礎類別,你也許會想要將各個模組共用的功能和視覺化介面集中在此類別以簡化各模組的撰寫工作,以及讓應用程式有一致的操作方式和行為,這部分每個人的需求不同,就請您自行發揮了。
如果你覺得以上的程式碼過於片段零散,無法獲得整體的概念,建議您直接看範例的原始碼,把範例程式執行一遍以觀察程式運作的過程,不了解的地方再回來文件裡尋找解釋,這樣也許會比較容易些。為了方便閱讀,我也把範例程式中比較重要的兩個單元分別列在表一和表二裡面了。
列表一. DllUtils.pas |
unit DllUtils; |
列表二. DllExport.pas |
unit DllExport; |
範例程式
範例程式可以按此處下載:PluginDLL.zip
下載壓縮檔並解開後,請先閱讀其中的 readme.txt。
可改進之處
你可以試著修改範例程式並強化它,使它可以當作實際開發專案的基礎框架,以下列出幾項可能的改進之處:
- 賦予 TBaseForm 基本的資料處理能力,像是新增、修改、刪除...等。
- 修改使之適用於 modeless form 及 MDI 應用程式。這意味著釋放 DLL 的時機也會改變,你可能會需要一個串列結構將載入的 DLL 記錄起來,通常一個 TStringList 就可以做到。
- 讓一個 plugin 物件可以建立並維護多個不同類型的 Form 物件。
你可能會希望一個 DLL 裡面可以提供多種 form 物件供主程式使用,這些 form 物件之間可能有某種程度的相似或相依關係。根據此需求我們可以整理出 plugin 物件具備以下兩個特性:
- plugin 物件可以建立多種不同類型的 form 物件,而它們都是繼承自基礎的表單類別 TBaseForm。
- 一個 DLL 裡面只需要一個 plugin 物件。
根據 [GHJV95] 書中的定義,Abstract Factory 的用意是:
「提供一個介面來建立同一族系或相依的物件,而毋須指明它們的具象類別(concrete class)」
而 Factory 通常也被實作成 Singleton,這些特性清楚地告訴我們 plugin 物件非常適合實作成一個 Factory。你可能需要在 TPlugin 類別裡面提供一個 RegisterClass 方法,這個方法取代了原先的類別參考型態,原本在 TBaseForm 子類別的單元裡設定 g_ConcreteClass 的敘述將會改成:
PluginFactory.RegisterClass(TForm1);
註冊過的類別資訊將會被記錄在一個串列裡面。主程式則可以在建立 form 物件時透過字串來指定要建立的 form 類別名稱,像這樣:
APlugin.CreateForm('TCustomerForm');
plugin 物件的 CreateForm 方法就會到串列中搜尋註冊過的類別,取得對應的類別參考並建立其實體(是不是有點像 COM 所做的事情?)。
嗯,我想這樣的提示應該夠了,最重要的還是要自己實際去撰寫及除錯程式碼以獲得更深刻的體會,真能如此,這個 Design Pattern 就會完全融入你的知識體系裡面,以後不加思索便可以運用自如了。
結語
在這份文件裡面主要是介紹以 Delphi 來設計 plugin 模組的實作過程,其中運用了介面程式設計的技巧(包括介面的參考計數以及物件生命週期的控制)以及 Design Patterns 來解決設計時遭遇的問題,這也是學習的重點之一。
在一個多人開發的專案裡,如果您的責任是設計主程式框架,當您要以 DLL 來切割應用程式時會怎麼做呢?這篇文章裡面展示了一種可能的設計方式,如果您有不同的想法或者對本文有任何建議,都很歡迎您來信指教。
Delphi 的 DLL 記憶體漏洞
最後,雖然不是本文的主題,但也頗值得注意的,就是動態載入的 DLL 在釋放時會有 4K 的記憶體漏洞,而且 Delphi 5 和 6 都有這個問題,你可以閱讀下面兩份文件,其中有詳細的說明並提供解決之道:
- Memory Lost And Found...And Release by Roy Nelson.
http://www.thedelphimagazine.com/samples/1328/1328.htm - VCL leak fix for dynamic DLLs by Dejan Maksimovic.
http://codecentral.borland.com/codecentral/ccweb.exe/listing?id=16380
註1.
| 由於 DLL 版本的更新可能使得原本叫用它的程式無法正常運作,因此以不同的檔名區分版本(例如:MFCxx.DLL),使得硬碟裡面必須保存同一種 DLL 的多個版本,即使使用者將應用程式移除了,卻不敢放心的移除相關的 DLL 檔案,以免其他應用程式因為缺少了這個檔案而無法運作,這種情況所形成的問題稱為 DLL hell。COM 的出現有解決此問題的企圖(透過執行時期詢問元件支援的介面),但似乎並不理想,直到 .NET 的問世而終於有了比較好的解決方案。 |
註2. | 可以到 http://www.geocities.com/huanlin_tsai/ 的〔心得分享〕區找到相關文章。不可諱言,以上所說的難免摻雜了個人的因素,也許其他人在使用 package 時並未發生上述問題,而且使用 package 的方式也有許多優點,在此僅將個人實際應用時的狀況與感覺描述出來,若有謬誤之處尚請各方不吝指正。 |
註3. | Singleton 樣式:提供單一窗口來建立類別的實體,以確保只有一個類別的實體存在。參考 [GHJV95] 的書。 |
參考資料
- Delphi 學習筆記。作者:錢達智。碁峰資訊,1998。
- [Cantu2001] Marco Cantu. Mastering Delphi 6. SYBEX, 2001.
- [Harmon2000] Eric Harmon. Delphi COM Programming. MTP, 2000.
- [GHJV95] E. Gamma, R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995. 中文版:物件導向設計模式,葉秉哲。培生,2001。
Read more ...
Implementing Plug-Ins for Your Delphi Applications
Implementing Plug-Ins for Your Delphi Applications
By Tim Sullivan - tim@uil.net
Back in January, 1999, Delphi Informant ran an article on developing plugins for your Delphi applications. It covered some of the basics of that plugins are, how to make them, exporting fucntions, and so on. I was tremendously inspired, because a system I was (and still am) working on could (and did!) benefit greatly from such a technology.
However, the layout that was described had some pretty big flaws. First, it wasn't really OOP. Second, it was a little hairy to pass information back and forth. Finally, you had to write a lot of the initialization stuff yourself. There had to be a better way.
A quick overview of what a plugin is: Plugins are DLL files. They contain additional commands or other functionality that can add to your system. Often they add menu items or toolbuttons to your application. Simply the existance of a file can radically enhance your software.
There was. Step 1 was to create an abstract, base class for a plugin. The plugin should know a bit about the application (for example, having a copy of the Application variable could be useful). It should know a bit about itself: how many "commands" it has, what the name of the plugin was, the author, etc. After some fuddling, this is what I came up with:
typeMost of the methods and properties are self explanatory. Each plugin publishes a number of commands (how many is returned by GetNumCommands). To get information about a specific command, a call to GetCommand will give you the command's caption, bitmap, hint and event. Notice the stdcall after each of the methods. This is required by the dll in order for it to work properly.
TuilPlugin = class(TObject)
private
FHostApplication : TApplication;
FFilename : string;
FManager : TComponent;
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create;
destructor Destroy; override;
function GetAuthor : String; virtual; stdcall;
function GetDescription : String; virtual; stdcall;
function GetName : String; virtual; stdcall;
function Initialize(Manager : TComponent; HostApplication : TApplication; Filename : string) : Boolean; virtual; stdcall;
function GetNumCommands : Integer; virtual; stdcall;
procedure GetCommand(index : integer; var Caption, Hint, Data : string; var Bitmap : HBitmap; var Event : TNotifyEvent); virtual; stdcall;
procedure Configure; virtual; stdcall;
{ properties }
property HostApplication : TApplication read FHostApplication;
property Filename : string read FFilename;
property Manager : TComponent read FManager;
end; { TuilPlugin }
The second thing that needed to be done was to develop a loader component, which would take care of all the drudgery of creating, initializing, destroying and generally managing the plugins. Here's what I came up with:
TuilPluginManager = class(TComponent)The main meat procedure here is LoadPlugin. It handles the actual loading and initialization of a plugin. LoadPlugins is useful as well, since it globally loads all the plugins in the application's folder.
private
// Private declarations
FExtension : String;
FPlugins : TList;
FOnBeforeLoading : TNotifyEvent;
FOnAfterLoading : TNotifyEvent;
FOnBeforeLoad : TuilBeforeLoadEvent;
FOnAfterLoad : TuilAfterLoadEvent;
FOnNewCommand : TNewCommandEvent;
protected
[...]
public
// Public declarations
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
procedure LoadPlugin(Filename : string); virtual;
procedure LoadPlugins; virtual;
procedure UnloadPlugin(index : integer); virtual;
procedure GetLoadedPlugins(PluginList : TStrings); virtual;
property Plugins[index : integer] : TuilPlugin read GetPlugins; default; // Public
property PluginCount : integer read GetPluginCount;
published
// Published properties and events
property Extension : String read GetExtension write SetExtension; // Published
property Version : string read GetVersion write SetVersion;
property OnBeforeLoading : TNotifyEvent read FOnBeforeLoading write FOnBeforeLoading;
property OnAfterLoading : TNotifyEvent read FOnAfterLoading write FOnAfterLoading;
property OnBeforeLoad : TuilBeforeLoadEvent read FOnBeforeLoad write FOnBeforeLoad;
property OnAfterLoad : TuilAfterLoadEvent read FOnAfterLoad write FOnAfterLoad;
property OnNewCommand : TNewCommandEvent read FOnNewCommand write FOnNewCommand;
end; // TuilPluginManager
All this is well and good, but how the heck do we make our OWN plugins? Actually, it's really simple.
First, you want to create a descendant of the TuilPlugin class. Include (private) event handlers for each of the commands you want to export.
typeThe two most important methods you override are GetNumCommands and GetCommand. GetNumCommands is easy. In this case, we've got 2 commands we're exporting:
TMyPlugin = class(TuilMyPlugin)
procedure Command1(Sender : TObject);
procedure Command2(Sender : TObject);
public
function GetAuthor : String; override; stdcall;
function GetDescription : String; override; stdcall;
function GetName : String; override; stdcall;
function Initialize(Manager : TComponent; HostApplication : TApplication; Filename : string) : Boolean; override; stdcall;
function GetNumCommands : Integer; override; stdcall;
procedure GetCommand(index : integer; var Caption, Hint, Data : string; var Bitmap : HBitmap; var Event : TNotifyEvent); override; stdcall;
procedure Configure; override; stdcall;
end;
function TMyPlugin.GetNumCommands : integer;GetCommand is a little trickier. You need to determine what command number you're getting, and return the appropriate information and event handler:
begin
Result := 2;
end;
procedure TupSamplePlugin.GetCommand(index : integer; var Caption, Hint, Data : string; var Bitmap : HBitmap; var Event : TNotifyEvent);That's most of it, believe it or not. We have to export a RegisterPlugin procedure with our dll, and include ShareMem as the first unit in both the DLL and our application's .DPR files.
begin
Caption := '';
Event := nil;
case index of
0 : begin
Caption := 'Command One';
Hint := 'Command One';
Data := '';
Event := CommandOne;
Bitmap := 0;
end;
1 : begin
Caption := 'Command Two';
Hint := 'Command Two';
Data := '';
Event := CommandTwo;
Bitmap := 0;
end;
end;
end;
Because there is so much to make sure you do (Sharemem, stdcall, RegisterPlugin, and so on), I put together a Wizard that would make things a lot easier.
I've included the complete source code for the system, including the wizard, available for free. If you are using Delphi 3, there is different code which you can get by clicking here. Feel free to play with it and let me know what you think. I'm hoping that we can continue to improve the system as a community. Comments are welcome!
Read more ...2008-04-25
Delphi Basics: Interface command
Version 1
It starts the definition of external interface of a Unit. Declarations here are externally visible by other units. All of these declarations must be implemented in the Implementation section.
The Uses statement, if present, must be at the start.
Version 2
In Object Oriented programming, we often use Abstract class methods in a base class as a placeholder. All derived classes must implement these methods.
Taking this one step further, an Interface defines a grouping of just abstract properties and methods. It provides a template for a class to use to ensure consistency. It is like a class with only abstract methods. It has the benefits that classes can be based on one parent class, and implement one or more interfaces. It adds a predictable flavour of operation to each class that implements the interface.
Take a look at the Delphi tutorial for more on this complex subject. Read more ...
Delphi Plugin Technology
--------------------------------------------------------------------------------
有没有使用过Adobe Photoshop?如果用过,你就会对插件的概念比较熟悉。对外行人来说,插件仅仅是从外部提供给应用程序的代码块而已(举个例子来说,在一个DLL中)。一个插件和一个普通DLL之间的差异在于插件具有扩展父应用程序功能的能力。例如,Photoshop本身并不具备进行大量的图像处理功能。插件的加入使其获得了产生诸如模糊、斑点,以及其他所有风格的奇怪效果,而其中任何一项功能都不是父应用程序自身所具有的。
对于图像处理程序来说这很不错,可是为什么要花偌大的力气去完成支持插件的商业应用程序呢?假设,我们举个例子,你的应用程序要产生一些报表。你的客户肯定会一直要求更新或者增加新的报表。你可以使用一个诸如Report Smith的外部报表生成器,这是个不怎么样的解决方案,需要发布附加的文件,要对用户进行额外的培训,等等。你也可以使用QuickReport,不过这会使你身处版本控制的噩梦之中——如果每改变一次字体你就要Rebuild你的应用程序的话。
然而,只要你把报表做到插件中,你就可以使用它。需要一个新的报表吗?没问题,只要安装一个DLL,下次应用程序启动时就会看见它了。另外一个例子是处理来自外部设备(比如条形码扫描器)的数据的应用程序,为了给用户更多的选择,你不得不支持半打的各种设备。通过将每种设备接口处理例程写成插件,不用对父应用程序作任何变动就可以获得最大程度的可伸缩性。
入门
在开始写代码之前最重要的事情就是搞清楚你的应用程序到底需要扩展哪些功能。这是因为插件是通过一个特定的接口与父应用程序交互的,而这个接口将根据你的需要来定义。在本文中,我们将建立3个插件,以便展示插件与父应用程序相交互的几种方式。
我们将把插件制作成DLL。不过,在做这项工作之前,我们得先制作一个外壳程序来载入和测试它们。显示的是加载了第一个插件以后的测试程序。第一个插件没有完成什么大不了的功能,实际上,它所做的只是返回一个描述自己的字符串。不过,它证明了很重要的一点——不管有没有插件应用程序都可以正常运行。如果没有插件,它就不会出现在已安装的插件列表中,但是应用程序仍然可以正常的行使功能。
我们的插件外壳程序与普通应用程序之间的唯一不同就在于工程源文件中出现在uses子句中的Sharemem单元和加载插件文件的代码。任何在自身与子DLL之间传递字符串参数的应用程序都需要Sharemem单元,它是DelphiMM.dll(Delphi提供该文件)的接口。要测试这个外壳,需要将DelphiMM.dll文件从Delphi\Bin目录复制到path环境变量所包含的路径或者应用程序所在目录中。发布最终版本时也需要同时分发该文件。
插件通过LoadPlugins过程载入到这个测试外壳中,这个过程在主窗口的FormCreate事件中调用。该过程使用FindFirst和FindNext函数在应用程序所在目录中查找插件文件。找到一个文件以后,就使用图3所示的LoadPlugins过程将其载入。
{ 在应用程序目录下查找插件文件 }
procedure TfrmMain.LoadPlugins;
var
sr: TSearchRec;
path: string;
Found: Integer;
begin
path := ExtractFilePath(Application.Exename);
try
Found := FindFirst(path + cPLUGIN_MASK, 0, sr);
while Found = 0 do begin
LoadPlugin(sr);
Found := FindNext(sr);
end;
finally
FindClose(sr);
end;
end;
{ 加载指定的插件 DLL. }
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
DescribeProc(Description);
memPlugins.Lines.Add(Description);
end
else
begin
MessageDlg('File "' + sr.Name +
'" is not a valid plug-in.',
mtInformation, [mbOK], 0);
end;
end
else
MessageDlg('An error occurred loading the plug-in "' +
sr.Name + '".', mtError, [mbOK], 0);
end;
LoadPlugin方法展示了插件机制的核心。首先,插件被写成DLL。其次,通过LoadLibrary API它被动态的加载。一旦DLL被加载,我们就需要一个访问它所包含的过程和函数的途径。API调用GetProcAddress提供这种机制,它返回一个指向所需例程的指针。在我们这个简单的演示中,插件仅仅包含一个名为DescribePlugin的过程,由常数cPLUGIN_DESCRIBE指定(过程名的大小写非常重要,传递到GetProcAddress的名称必须与包含在DLL中的例程名称完全一致)。如果在DLL中没有找到请求的例程,GetProcAddree将返回nil,这样就允许使用Assigned函数测定返回值。
为了以一种易用的方式存储指向一个函数的指针,有必要为用到的变量创建一个特定的类型。注意,GetProcAddress的返回值被存储在一个变量中,DescribeProc,属于TpluginDescribe类型。下面是它的声明:
type
TPluginDescribe = procedure(var Desc: string); stdcall;
由于过程存在于DLL内部,它通过标准调用转换编译所有导出例程,因此需要使用stdcall指示字。这个过程使用一个var参数,当过程返回的时候它包含插件的描述。
要调用刚刚获得的过程,只需要使用保存地址的变量作为过程名,后面跟上任何参数。就我们的例子而言,声明:
DescribeProc(Description)
将会调用在插件中获得的描述过程,并且用描述插件功能的字符串填充Description变量。
构造插件
我们已经创建好了父应用程序,现在该轮到创建我们希望加载的插件了。插件文件是一个标准的Delphi DLL,所以我们从Delphi IDE中创建一个新DLL工程,保存它。由于导出的插件函数将用到字符串参数,所以要在工程的uses子句中把Sharemen单元放在最前面。列出的就是我们这个简单插件的工程源文件。
uses
Sharemem, SysUtils, Classes,
main in 'main.pas';
{$E plg.}
exports
DescribePlugin;
begin
end.
虽然插件是一个DLL文件,但是没有必要一定要给它一个.DLL的扩展名。实际上,一个原因就足以让我们有理由改变扩展名:当父应用程序寻找要加载的文件时,新的扩展名可以作为特定的文件掩模。通过使用别的扩展名(我们的例子使用了*.plg),你可以在一定程度上确信应用程序只会载入相应的文件。编译指示字$X可以实现这个改变,也可以通过Project Options对话框的Application页来设置扩展名。
第一个例子插件的代码是很简单的。显示了包含在一个新单元中的代码。注意,DescribePlugin原型与外壳应用程序中的TpluginDescribe类型相一致,使用附加的export保留字指定该过程将被导出。被导出的过程名称也将会出现在主工程源代码的exports段中(在中列出)。
unit main;
interface
procedure DescribePlugin(var Desc: string);
export; stdcall;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := 'Test plugin v1.00';
end;
end.
在测试这个插件之前,要先把它复制到主应用程序的路径下。最简单的办法就是在主目录的子目录下创建插件,然后把输出路径设置为主路径(Project Options对话框的Directories/Conditionals也可以作这个设置)。
调试
现在介绍一下Delphi 3中一个较好的功能:从IDE中调试DLL的能力。在DLL工程中可以通过Run paramaters对话框指定某程序为宿主应用程序,这就是指向将调用DLL的应用程序的路径(在我们这个例子中,就是刚刚创建的测试外壳程序)。然后你就可以在DLL代码中设置断点并且按F9运行它——就像在一个普通应用程序中做的那样。Delphi会运行指定的宿主程序,并且,通过编译带有调试信息的DLL,把你指引到DLL代码内的断点处。
//////////////////////////////////////////////////////////////////////
Delphi 插件(Plug-ins)创建、调试与使用应用程序扩展(续)
作者:不详
文章来源:China ASP
上一篇 下一篇
相关文档:
Delphi 插件(Plug-ins)创建、调试与使用应用程序扩展
--------------------------------------------------------------------------------
延伸父应用
这个简单的插件不错,不过它不能做什么有用的事情。第二个例子就是纠正这个问题。这个插件的目标就是在父应用程序的主菜单中加入一个项目。这个菜单项目,当被单击时,就会执行插件内的一些代码。显示外壳程序的改进版,两个插件都已经加载。在这个版本的外壳程序中,一个名为Plug-in的新菜单项目,被添加到主菜单中。插件会在运行时加入一个菜单项。
为了实现这个目的,我们必须在插件DLL中定义第二个接口。现有的DLL只导出了一个过程,DescribePlugin。第二个插件将声明一个叫做InitPlugin的过程。不过,在这个过程可以在主应用程序中看到以前,必须修改LoadPlugin来配合它。
所示的代码展示了改进的过程。
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
InitProc: TPluginInit;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
// 查找 DescribePlugin.
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
// 调用 DescribePlugin.
DescribeProc(Description);
memPlugins.Lines.Add(Description);
// 查找 InitPlugin.
InitProc := GetProcAddress(LibHandle, cPLUGIN_INIT);
if Assigned(InitProc) then
begin
// 调用 InitPlugin.
InitProc(mnuMain);
end;
end
else
begin
MessageDlg('File "' + sr.Name +
'" is not a valid plugin.',
mtInformation, [mbOK], 0);
end;
end
else
begin
MessageDlg('An error occurred loading the plugin "' +
sr.Name + '".', mtInformation, [mbOK], 0);
end;
end;
如你所见,当GetProcAddress第一次查找调用描述过程之后,又调用了一次GetProcAddress。这一次,我们要寻找的是常量cPLUGIN_INIT,定义如下:
const
cPLUGIN_INIT = 'InitPlugin';
返回值存储在TpluginInit类型的变量中,定义如下:
type
TPluginInit = procedure(ParentMenu: TMainMenu); stdcall;
当InitPlugin方法被执行时,父应用程序的主菜单被当作一个参数传递给它。这个过程可以按照自己的意愿修改菜单。由于所有GetProcAddress的返回值都用assigned测试,新版本的LoadPlugin过程仍然会加载不包含InitPlugin过程的第一个插件。在这个过程中第一次调用寻找DescribePlugin方法会通过,第二次寻找InitPlugin会无响应失败。
现在新的接口已经定义好了,可以为新的InitPlugin方法编写代码了。像原先一样,新插件的实现代码存在于一个单独的单元中。显示了修改过的包含InitPlugin方法的main.pas。
unit main;
interface
uses Dialogs, Menus;
type
THolder = class
public
procedure ClickHandler(Sender: TObject);
end;
procedure DescribePlugin(var Desc: string);
export; stdcall;
procedure InitPlugin(ParentMenu: TMainMenu);
export; stdcall;
var
Holder: THolder;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := 'Test plugin 2 - Menu test';
end;
procedure InitPlugin(ParentMenu: TMainMenu);
var
i: TMenuItem;
begin
// 创建新菜单项.
i := NewItem('Plugin &Test', scNone, False, True,
Holder.ClickHandler, 0, 'mnuTest');
ParentMenu.Items[1].Add(i);
end;
procedure THolder.ClickHandler;
begin
ShowMessage('Clicked!');
end;
initialization
Holder := THolder.Create;
finalization
Holder.Free;
end.
第二个插件的代码
很明显,对原始插件的第一个改变就是增加了InitPlugin过程。像原先一样,带有export关键字的原型被加入到单元顶端的列表中,过程名也被加入到工程源代码的exports子句列表中。这个过程使用NewItem函数创建一个新的菜单项,返回值是TmenuItem对象。新菜单项通过下列语句被加入到应用程序主菜单中:
ParentMenu.Items[1].Add(I);
在测试外壳主菜单上的Items[1]是菜单项Plug-in,所以这个语句在Plugin菜单条上添加一个叫Plug-in Test的菜单项。
为了处理对新菜单项的响应,作为它的第五个参数,NewItem可以接受一个TNotifyEvent类型的过程,这个过程将在菜单项被点击时调用。不幸的是,按照定义,这种类型的过程是一个对象方法,然而在我们的插件中并没有对象。如果我们想用通常的指针来指向函数,那么得到的将只会是Delphi编译器的抱怨。所以,唯一的解决办法就是创建一个处理菜单点击的对象。这就是Tholder类的用处。它只有一个方法,是一个叫做ClickHandler的过程。一个叫做Holder的全局变量,在修改过的main.pas的var段中被声明为Tholder类型,并且在单元的initialization段中被创建。现在我们就有一个对象了,我们可以拿它的方法(Holder.ClickHandler)当作NewItem函数的参数。
搞了这一通,ClickHandler除了显示一个“Clicked!”消息对话框以外什么以没干。也许这不怎么有趣,不过它仍然证明了一点:插件DLL成功的修改了父应用的主菜单,表现了它的新用途。并且如同第一个例子一样,不管这个插件在不在应用程序都能执行。
由于我们创建了一个对象来处理菜单点击,那么在不再需要这个插件时,就要释放这个对象。修改后的单元中会在finalization段中处理这件事情。Finalization端时与initialization段相对应的,如果前面有一个initialization段,那么在应用程序终止时finalization段一定会得到执行。把下面的语句
Holder.Free
加到finalization段中,以确保Holder对象会被正确的释放。
显而易见,虽然这个插件只是修改了外壳应用的主菜单,但是它可以轻易地操纵传递到InitPlugin过程中的任何其他对象。如果有必要,插件也可以打开自己的对话框,向列表框(List boxes)和树状视图(tree views)中添加项目,或者在画布(canvas)中绘画。
事件驱动的插件
到现在为止我们所描述的技术可以产生一种通用的扩展应用程序的方法。通过增加新菜单、窗体和对话框,就可以实现全新的功能而不必对父应用做任何修改。不过仍然有一个限制:这只是一种单侧(one-sided)机制。正如所看到的,系统依赖用户的某些操作才能启动插件代码,比如点击菜单或者类似的动作。代码运行起来以后,又要依靠另外一个用户动作来停止它,例如,关闭插件可能已经打开的窗体。克服这种缺陷的一种可行的方法就是使插件可以响应父应用中的动作——模仿在Delphi中工作地很好的事件驱动编程模型的确有效。
在最后一个例子插件中,我们将创建一种机制,插件可以藉此响应父应用中产生的事件。通常情况下,可以通过判定需要触发哪些事件、在父应用中为每个事件创建一个Tlist对象来实现。然后每个Tlist对象都被传递到插件的初始化过程中,如果插件想在某个事件中执行动作,它就把负责执行的函数地址加入到对应的TList中。父应用在适当的时刻循环这些函数指针的列表,按次序调用每个函数。通过这种方法,就为多个插件在同一事件中执行动作提供了可能。
应用程序产生的事件完全依赖于程序已确定的功能。例如,一个TCP/IP网络应用程序可能希望通过TclientSocket的onRead事件通知插件数据抵达,而一个图形应用程序可能对调色板的变化更感兴趣。
为了说明事件驱动的插件应答的概念,我们将创建一个用于限制主窗口最小尺寸的插件。这个例子有点儿造作,因为把这个功能做到应用程序里边会比这简单的多。不过这个例子的优点在语容易编码而且易于理解,而这正是本文想要做到的。
很明显,我们要做的第一件事情就是决定到底要产生哪些事件。在本例中,答案很简单:要限制一个应用程序窗口的尺寸,有必要捕获并且修改Windows消息WM_GETMINMAXSINFO。因此,要创建一个完成这项功能的插件,我们必须捕获这个消息并且在这个消息处理器中调用插件例程。这就是我们要创建的事件。
接下来我们要创建一个TList来处理这个事件。在主窗体的initialization段中将会创建lstMinMax对象,然后,创建一个消息处理器来捕获Windows消息WM_GETMINMAXINFO。
{ 捕获 WM_GETMINMAXINFO. 为每个消息调用插件例程. }
procedure TfrmMain.MinMaxInfo(var msg: TMessage);
var
m: PMinMaxInfo; file://在 Windows.pas 中定义.
i: Integer;
begin
m := pointer(msg.Lparam);
for i := 0 to lstMinMax.count -1 do begin
TResizeProc(lstMinMax[i])(m.ptMinTrackSize.x,
m.ptMinTrackSize.y);
end;
end;
WM_GETMINMAXINFO 的消息处理器
外壳应用的LoadPlugin过程必须再次修改以便调用初始化例程。这个新初始化函数把我们的TList当作参数接受,在其中加入修改消息参数的函数地址。图1显示了LoadPlugin过程的最终版本,它可以执行到目前为止所讨论的全部几个插件的初始化工作。
{ 加载指定的插件DLL. }
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
InitProc: TPluginInit;
InitEvents: TInitPluginEvents;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
// 查找 DescribePlugin.
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
// 调用 DescribePlugin.
DescribeProc(Description);
memPlugins.Lines.Add(Description);
file://查找InitPlugin.
InitProc := GetProcAddress(LibHandle, cPLUGIN_INIT);
if Assigned(InitProc) then
begin
file://调用InitPlugin.
InitProc(mnuMain);
end;
// 为第三方插件查找 InitPluginEvents
InitEvents := GetProcAddress(LibHandle,
cPLUGIN_INITEVENTS);
if Assigned(InitEvents) then
begin
// 调用 InitPlugin.
InitEvents(lstMinMax);
end;
end
else
begin
MessageDlg('File "' + sr.Name +
'" is not a valid plugin.',
mtInformation, [mbOK], 0);
end;
end
else
begin
MessageDlg('An error occurred loading the plugin "' +
sr.Name + '".', mtInformation, [mbOK], 0);
end;
end;
图 1: LoadPlugin 的最终版本
最后一步是创建插件自身。如同前面的几个例子,插件展示一个标志自身的描述过程。它也带有一个初始化例程,在本例中只是接受一个TList作为参数。最后,它还包含一个没有引出(Export)的历程,叫做AlterMinTrackSize,它将修改传递给它的数值。图2显示了最终插件的完整代码。
unit main;
interface
uses Dialogs, Menus, classes;
procedure DescribePlugin(var Desc: string);
export; stdcall;
procedure InitPluginEvents(lstResize: TList);
export; stdcall;
procedure AlterMinTrackSize(var x, y: Integer); stdcall;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := 'Test plugin 3 - MinMax';
end;
procedure InitPluginEvents(lstResize: TList);
begin
lstResize.Add(@AlterMinTrackSize);
end;
procedure AlterMinTrackSize(var x, y: Integer);
begin
x := 270;
y := 220;
end;
end.
图 2: 最终插件的代码
InitPluginEvents过程是这个插件的初始化例程。它接受一个TList作为参数。这个TList就是在父应用程序中创建的保存相应函数地址的列表。下面的语句:
lstResize.Add(@AlterMinTrackSize);
把AlterMinTrackSize函数的地址加入到了这个列表中。它被声明为类型stdcall以便与其他过程相配,不过用不着export指示字。由于函数被直接通过它的地址调用,所以也就没有必要按照通常的方式把它从DLL中引出。
所以,事件序列如下所列:
1、 在应用程序初始化时,创建一个TList对象。
2、 在启动时这个列表被传递到插件的初始化过程InitPluginEvents中。
3、 插件过程把一个过程的地址加入到列表中。
4、 每次窗口大小改变时所产生的Windows消息WM_GETMINMAXINFO被我们的应用程序所捕获。
5、 该消息被我们的消息处理器TfrmMain.MainMaxInfo所处理,见图1。
6、 消息处理器遍历列表并调用它所包含的函数,把当前的X和Y最小窗口尺寸作为参数传递。要注意,TList类只是存储指针,所以如果想用保存的地址做些什么事情的话,我们必须把指针转换成所需要的类型——在本例中,要转换成TresizeProc。
TResizeProc = procedure (var x, y: Integer); stdcall;
7、 插件过程AlterMinTrackSize(列表中的指针所指向的),接受X和Y值作为可变的var参数并且修改它们。
8、 控制权返回到父应用的消息处理器,按照最小窗口尺寸的新值继续运行下去。
9、 应用程序退出时TList会在主代码的finalization段被释放。
结论
使用该体系结构时,可能利用Delphi提供的package功能是个不错的主意。在通常情况下,我不是一个分割运行时模块的狂热爱好者,但是当你认为任一包含大量代码的Delphi DLL超过200KB时,它就开始变得有意义了。
这篇文章应该还是有些用处的,至少它可以让你思考一些程序设计方面的问题,比如如何让它变得更加灵活。我知道如果我在以前的应用程序中使用一些这种技术的话,我就可以省掉在修改程序方面的好多工作。我并不想把插件作为一种通用的解决方案。很明显,有些情况下额外的复杂度无法验证其正确性,或者应用程序压根儿就不打算把自身搞成几块可扩展的单元。还有一些其它的方法也可以达成同样的效果。Delphi自身提供了一个接口来创作能集成到IDE中的模块,比起我所说明的技术这种方法更加面向对象(或者说更“干净”),而我也确信你可以在自己的应用中模仿这一技术。在运行时加载Delphi包也不是做不到的。探索一下这种可能性吧。
[本文所介绍的技术在Delphi 4下工作的很好。实际上,Delphi 4增加了工程选项,使这类应用程序加强DLL(application-plus-DLL)的开发变得更加容易了。] Read more ...
2008-04-03
Firefox 2 or Firefox 3? it's a problem.
My firefox2 always crash when popup a new window.
Firefox 2 or Firefox 3? it's a problem.
