最近搞Delphi,發現原來的程序中使用了很多包,但是總是處於懵懵懂懂的狀態。索性來好好研究一下這個問題,可能要花一些時間。所以首先把需要分析的問題列出來:
什麼是包?什麼是exe?它們在組成上有什麼不同?包跟dcu是什麼關係? dcp是乾什麼的?這些文件在編譯時是什麼關係?又是怎麼裝載的?裝載了以後怎麼樣操作包? dll可以exports,但是為什麼delphi幫助中不提包的exports,但是有些代碼卻又在包中使用exPRots?
首先來看看delphi的編譯過程。 delphi的工程中有兩類:包和程序,前者的後綴為dpk,後者為dpr。從簡單的開始,先來搞dpr。根據delphi的幫助文檔,一個典型的dpr文件的結構如下:
1 program Editor;
2
3 uses
4 Forms, {change to QForms in linux}
5 REAbout in 'REAbout.pas' {AboutBox},
6 REMain in 'REMain.pas' {MainForm};
7
8 {$R *.res}
9
10 begin
11 application.Title := 'Text Editor';
12 Application.CreateForm(TMainForm, MainForm);
13 Application.Run;
14 end.
其中10行到14行,begin…end很自然就是程序的執行入口。 uses部分指明了程序需要使用的一些Unit,這個就比較含糊了,為什麼有的會用in指明源代碼的位置(這部分是自己向工程中添加的),有的如Forms這個部分,卻又不需要?那每個Unit又會uses其它Unit,這個問題似乎越來越複雜了。先看整個源代碼的結構:
我猜測編譯器第一步首先遍歷這張有向圖,對每個Unit,如果有必要就對其進行編譯,生成對應的dcu。而這個“必要”問題,我開始以為是use這個Unit的語句是帶有in的,後來試驗發現不對。因為在上面的情況下,Unit3並沒有在Unit1的Uses子句中指明路徑,但是仍然正確產生了對應的dcu文件。後來使用filemon來監視文件打開情況,發現過程是這樣的:對於圖中的每個節點,編譯器按照當前目錄—project屬性中的search path—IDE環境中的library path,這樣的順序,搜索節點對應的pas文件,沒找到就再來一遍,但是這次搜索的是節點對應的dcu文件。
現在編譯搞定了,每個Unit(即pas文件)已經生成了對於的dcu文件,下面的問題是連接。說到連接,問題就複雜了,連接有兩種:靜態和動態。靜態連接就是說把這些dcu全部合併到一起。這樣,一個Unit對另一個Unit的調用,就成了程序內部的事情了。這樣的好處是快,而且簡單,並發共享之類的問題都容易處理。缺點是目標程序很大,而且如果現在要編寫另一個程序,而Unit3可以重用的話,則在連接時Unit3.dcu被再次拷貝。這樣在兩個程序同時運行時,內存中會有兩個Unit3的副本,比較浪費。動態連接就是說,兩個程序在連接時,僅僅只保留對Unit3的引用,而並不拷貝Unit3的內容。到運行時,把Unit3裝入內存,讓兩個程序公用。 Dll和BPL都是動態連接的解決方案。問題在於,delphi中關於連接的選項就只有project|Options|packages菜單中出現,“Build with runtime packages”這句話實在是太模糊了。所以還要再研究一下。
在程序執行的時候,我們可以通過view|debug window|moudles來查看有哪些東西被加載到內存中去了,它們又包含哪些內容。
簡便起見,我們建立如下結構的一個程序:
program ProjectEXE;
uses
Forms,
Windows,
UnitFormMain in 'UnitFormMain.pas' {FormMain};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TFormMain, FormMain);
Application.Run;
end.
unit UnitFormMain;
interface
uses
Windows, StdCtrls, Forms, UnitFormAnother,Classes, Controls;
type
TFormMain = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
FormMain: TFormMain;
implementation
{$R *.dfm}
procedure TFormMain.Button1Click(Sender: TObject);
var
LForm:TFormAnother;
begin
LForm:=TFormAnother.Create(Application);
LForm.ShowModal;
LForm.Free;
end;
end.
unit UnitFormAnother;
interface
uses
Forms;
type
TFormAnother = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end;
implementation
{$R *.dfm}
end.
現在來動態。 “Build with runtime packages”打鉤,現在發現運行時ProjectEXE.exe文件只包含四個部分:兩個Form、一個SysInit.pas、一個ProjectEXE.dpr;與此同時進程樹里面多了兩個bpl:rtl60和vcl60,它們的內容就是剛才靜態連接中出現的那些Unit。現在ProjectEXE.exe只有16k。也就是說,有向圖中的Unit,一部分放在exe中了,另一部分放在bpl中了。但是根據什麼來劃分呢?是根據uses子句,還是根據這裡“Build with runtime packages”中的列表?繼續測試,發現:如果列表中僅包含vcl60,則加載到內存中的還是兩個bpl加一個exe;如果列表中只包含rtl60,則內存中僅包含rtl60和exe,但是exe的內容髮生了變化:裡面的Unit增多了,而且基本都是vcl60包裡面的。我猜想應該是rtl和vcl包之間存在require關係。這個留到下一步再測試。但是初步估計連接過程中,肯定會利用包列表,將那些已經在包中存在的Unit從exe中排除出去。
在動態連接之後,還存在一個問題:裝入。裝入有兩種策略,靜態也稱為自動,由delphi生成代碼,在裝載exe之前,自動裝入包;另一種是動態,即在程序運行時通過編碼,指定一個包,把它裝入內存。問題在於,我必須搞清楚delphi在什麼情況下會自動裝入一個包,什麼情況下可以避免delphi自作聰明,這樣才能靈活地使用包。前面的試驗中,只可以看出,在dpr文件執行到begin之前,靜態連接的的包就已經裝入內存了。具體過程我也不清楚,等下一章開始寫自己的包,再來做實驗吧。