参考:
Creating a DLL with Delphi for LoadRunner
http://ptfrontline.wordpress.com/2010/04/13/creating-a-dll-with-delphi-for-loadrunner/
This article extends my previous post on Using a Custom DLL in LoadRunner and now shows by example how a DLL is actually created in Delphi and used from LoadRunner.
The DLL I’ll create here is a simple stub with a few basic functions and can serve as a template for any type of DLL that you want to create.
I’ll show how to avoid the most common problems of sending/retrieving strings to/from the DLL as well as how to handle the Unicode situation with D2009 and D2010 (LR is not Unicode enabled).
Some of the things a LR DLL developer needs to remember are:
- LR is Muti-Threaded and Multi-processed, make the DLL thread-safe
- When using LoadGenerators the DLL must be distributed to several machines
- When using Performance Center the lr_load_dll() function needs to be white-listed on every LoadGenerator. The file to manipulate is “C:\Program Files\HP\Load Generator\merc_asl\lrun_api.asl”
- The DLL may be unloaded at any time, if the script fails/aborts (=> YOU must release allocated memory)
- If eternal loops or lockups inside the DLL occur, the whole controller/loadgenerator is lost until rebooted (really bad when under PerfCenter)
Keeping the above in mind, also remember the following general rules
- LR script makers make mistakes too by sending INVALID input to functions or using them in an unintended way. This means that you as a DLL developer NEED TO VERIFY the input params for every function call!
- The DLL may be installed on a variety of windows platforms (Win2000->Win7) so make sure you check the OS Version if you are using Windows API calls that are version dependent
- There is no guarantee that the DLL has Administrator privilidges!
- The DLL is restricted to the same Win32 memory as the host process (2 Gig max allocation)
Creating the DLL in Delphi
Close all projects and then choose “FILE | OTHER …“, select “DELPHI PROJECTS” and then “Dynamic Link Library“. You should now have the following skeleton (excluding all comments):
1 |
library Project1; |
2 |
uses |
3 |
SysUtils, |
4 |
Classes; |
5 |
{$R *.res} |
6 |
begin |
7 |
end . |
Now copy & paste the following instead of the above:
001 |
library MyDLL; |
002 |
|
003 |
uses |
004 |
SysUtils, Classes, Windows, |
005 |
VUManager in 'VUManager.pas' ; |
006 |
|
007 |
Const |
008 |
{ Version Constants } |
009 |
DLLVer_Major = 1 ; |
010 |
DLLVer_Minor = 0 ; |
011 |
DLLVer_Copyright = 'Copyright (C) by Celarius Oy' ; |
012 |
|
013 |
{ DLL Errors - Negatives are errors } |
014 |
ERR_None = 0 ; |
015 |
ERR_Unknown = - 1 ; |
016 |
ERR_Unknown_Object = - 2 ; |
017 |
ERR_Insufficient_Buffer = - 3 ; |
018 |
|
019 |
//////////////////////////////////////////////////////////////////////////////// |
020 |
|
021 |
Var |
022 |
VUMgr : TVUManager; |
023 |
|
024 |
{$R *.res} |
025 |
|
026 |
//////////////////////////////////////////////////////////////////////////////// |
027 |
|
028 |
function dll_Initialize( VUserID: Cardinal ): Integer ; stdcall; |
029 |
// Initialize DLL, Adds the Vuser with the ID |
030 |
// |
031 |
// Returns |
032 |
// Integer The Error code |
033 |
begin |
034 |
If VUMgr . AddVUser( VUserID ) then |
035 |
Result := ERR_None |
036 |
Else |
037 |
Result := ERR_Unknown_Object; |
038 |
end ; |
039 |
|
040 |
//////////////////////////////////////////////////////////////////////////////// |
041 |
|
042 |
function dll_Finalize( VUserID: Cardinal ): Integer ; stdcall; |
043 |
// Finalize DLL, removes the VUser with the ID |
044 |
// |
045 |
// Returns |
046 |
// Integer Error code |
047 |
begin |
048 |
If VUMgr . RemoveVUser( VUserID ) then |
049 |
Result := ERR_None |
050 |
Else |
051 |
Result := ERR_Unknown_Object; |
052 |
end ; |
053 |
|
054 |
//////////////////////////////////////////////////////////////////////////////// |
055 |
|
056 |
function dll_GetVersion: Integer ; stdcall; |
057 |
// Get Library version |
058 |
// |
059 |
// Returns |
060 |
// Integer Version Number |
061 |
begin |
062 |
Result := (DLLVer_Major SHL 16 ) + DLLVer_Minor; |
063 |
end ; |
064 |
|
065 |
//////////////////////////////////////////////////////////////////////////////// |
066 |
|
067 |
function dll_GetCopyright( DestStr: PAnsiChar ; DestSize: Integer ): Integer ; stdcall; |
068 |
// Get Library Copyright String |
069 |
// |
070 |
// Returns |
071 |
// Integer Version Number |
072 |
begin |
073 |
if (DestSize>Length(DLLVer_Copyright)) then |
074 |
Begin |
075 |
StrCopy( DestStr, PAnsiChar (DLLVer_Copyright) ); |
076 |
Result := ERR_None; |
077 |
End Else Result := ERR_Insufficient_Buffer; |
078 |
end ; |
079 |
|
080 |
//////////////////////////////////////////////////////////////////////////////// |
081 |
|
082 |
function dll_TrimStr(ID: Cardinal ; SourceStr: PAnsiChar ; DestStr: PAnsiChar ; DestSize: Integer ): Integer ; stdcall; |
083 |
// Trims a String (removes special chars in beginning/end) |
084 |
// |
085 |
// Returns |
086 |
// Integer Error Code |
087 |
Var |
088 |
VU : TVirtualUser; |
089 |
S : AnsiString ; |
090 |
begin |
091 |
{ Find the Virtual User } |
092 |
VU := VUMgr . FindVU(ID); |
093 |
if Assigned(VU) then |
094 |
Begin |
095 |
S := VU . TrimString( SourceStr ); // process the string |
096 |
|
097 |
if (DestSize>Length(S)) then |
098 |
Begin |
099 |
StrCopy( DestStr, PAnsiChar (S) ); |
100 |
Result := ERR_None; |
101 |
End Else Result := ERR_Insufficient_Buffer; |
102 |
|
103 |
End Else Result := ERR_Unknown_Object; |
104 |
end ; |
105 |
|
106 |
//////////////////////////////////////////////////////////////////////////////// |
107 |
|
108 |
{ Export functions } |
109 |
exports |
110 |
{ General Functions } |
111 |
dll_Initialize name 'dll_Initialize' , |
112 |
dll_Finalize name 'dll_Finalize' , |
113 |
dll_GetVersion name 'dll_GetVersion' , |
114 |
dll_GetCopyright name 'dll_GetCopyright' , |
115 |
dll_TrimStr name 'dll_TrimStr' |
116 |
; |
117 |
|
118 |
//////////////////////////////////////////////////////////////////////////////// |
119 |
|
120 |
procedure DLLEntryProc(EntryCode: integer ); |
121 |
// |
122 |
// DLL Entry/Exit, Process and Thread Attach/Detach procedures } |
123 |
// |
124 |
Var |
125 |
Buf : Array [ 0.. MAX_PATH] of Char ; |
126 |
LoaderProcess : String ; |
127 |
DLLPath : String ; |
128 |
begin |
129 |
Try |
130 |
Case EntryCode of |
131 |
{ The DLL has just been loaded with LoadLibrary() } |
132 |
{ either by the main process, or by a vuser - we do not know } |
133 |
{ which one } |
134 |
DLL_PROCESS_ATTACH: |
135 |
Begin |
136 |
{ Get the Path where the DLL is } |
137 |
GetModuleFileName(HInstance, buf, Length(buf)) ; |
138 |
DLLPath := ExtractFilePath(StrPas(buf)); |
139 |
{ Get the name of the module that loaded us } |
140 |
GetModuleFileName(HInstance, buf, Length(buf)) ; |
141 |
LoaderProcess := StrPas(buf); |
142 |
{ Create handler } |
143 |
VUMgr:= TVUManager . Create( NIL ); |
144 |
End ; |
145 |
|
146 |
{ The Process Detached the DLL - Once per MMDRV Process } |
147 |
{ Application called Unload - Perform Cleanup } |
148 |
DLL_PROCESS_DETACH: |
149 |
Begin |
150 |
If Assigned(VUMgr) then FreeAndNil(VUMgr); |
151 |
End ; |
152 |
|
153 |
{ A Thread has just loaded the DLL } |
154 |
DLL_THREAD_ATTACH: |
155 |
Begin |
156 |
{ This occures when a VUser calls lr_load_dll() } |
157 |
End ; |
158 |
|
159 |
{ A Thread has just unloaded the DLL } |
160 |
DLL_THREAD_DETACH: |
161 |
Begin |
162 |
{ This occures when a VUser exits } |
163 |
End ; |
164 |
End ; |
165 |
Except |
166 |
// TODO: Write an Exception Handler |
167 |
On E: Exception do ; |
168 |
End ; |
169 |
end ; |
170 |
|
171 |
//////////////////////////////////////////////////////////////////////////////// |
172 |
// |
173 |
// DLL Initialization - When DLL is loaded the first time by any process } |
174 |
// |
175 |
//////////////////////////////////////////////////////////////////////////////// |
176 |
begin |
177 |
{$IFDEF Debug} |
178 |
{ True means Delphi checks & reports memleaks } |
179 |
ReportMemoryLeaksOnShutdown := True ; |
180 |
{ $ENDIF } |
181 |
{ Make the Memory-Manager aware that this DLL is used in Thread-environments } |
182 |
IsMultiThread := True ; |
183 |
VUMgr:= NIL ; |
184 |
|
185 |
{ If we have not already set te DLLProc, do it } |
186 |
if NOT Assigned(DllProc) then |
187 |
begin |
188 |
DLLProc := @DLLEntryProc; // install custom exit procedure |
189 |
DLLEntryProc(DLL_PROCESS_ATTACH); |
190 |
end ; |
191 |
end . |
You can now save the project as “MyDLL” in a folder of your choice.
Now we’ll need to create the VUManager unit with “FILE | NEW UNIT” and save the new unit as “VUManager.pas” in the same directory as the rest of the Delphi files. Copy & Paste the following into the new unit.
001 |
unit VUManager; |
002 |
|
003 |
Interface |
004 |
|
005 |
Uses SysUtils, Classes, Windows; |
006 |
|
007 |
Type |
008 |
{ Forward Declarations } |
009 |
TVirtualUser = class ; |
010 |
TVUManager = class ; |
011 |
|
012 |
//////////////////////////////////////////////////////////////////////////////// |
013 |
|
014 |
TVirtualUser = class (TComponent) |
015 |
private |
016 |
{ Private declarations } |
017 |
protected |
018 |
{ Protected declarations } |
019 |
public |
020 |
{ Public declarations } |
021 |
VUserID : Cardinal ; |
022 |
Constructor Create(AOwner:TComponent; ID: Cardinal ); ReIntroduce; |
023 |
Destructor Destroy; Override; |
024 |
Function TrimString(InStr: AnsiString ): AnsiString ; |
025 |
end ; |
026 |
|
027 |
//////////////////////////////////////////////////////////////////////////////// |
028 |
|
029 |
TVUManager = class (TComponent) |
030 |
private |
031 |
{ Private declarations } |
032 |
fVirtualUsers : TThreadList; |
033 |
protected |
034 |
{ Protected declarations } |
035 |
public |
036 |
{ Public declarations } |
037 |
Constructor Create(AOwner:TComponent); Override; |
038 |
Destructor Destroy; Override; |
039 |
{ Methods } |
040 |
Function FindVU(ID: Cardinal ):TVirtualUser; |
041 |
Function AddVUser(ID: Cardinal ): Boolean ; |
042 |
Function RemoveVUser(ID: Cardinal ): Boolean ; |
043 |
end ; |
044 |
|
045 |
//////////////////////////////////////////////////////////////////////////////// |
046 |
|
047 |
Implementation |
048 |
|
049 |
//////////////////////////////////////////////////////////////////////////////// |
050 |
|
051 |
{ TVirtualUser } |
052 |
|
053 |
constructor TVirtualUser . Create(AOwner: TComponent; ID: Cardinal ); |
054 |
begin |
055 |
inherited Create(AOwner); |
056 |
VUserID := ID; |
057 |
end ; |
058 |
|
059 |
destructor TVirtualUser . Destroy; |
060 |
begin |
061 |
Try |
062 |
{ Cleanup anything that is allocated by the VUser } |
063 |
Finally |
064 |
inherited ; |
065 |
End ; |
066 |
end ; |
067 |
|
068 |
function TVirtualUser . TrimString(InStr: AnsiString ): AnsiString ; |
069 |
begin |
070 |
Result := Trim(InStr); |
071 |
end ; |
072 |
|
073 |
//////////////////////////////////////////////////////////////////////////////// |
074 |
|
075 |
{ TVUManager } |
076 |
|
077 |
constructor TVUManager . Create(AOwner: TComponent); |
078 |
begin |
079 |
Try |
080 |
inherited ; |
081 |
fVirtualUsers := TThreadList . Create(); |
082 |
Finally |
083 |
End ; |
084 |
end ; |
085 |
|
086 |
destructor TVUManager . Destroy; |
087 |
Var |
088 |
I : Integer ; |
089 |
begin |
090 |
Try |
091 |
{ Clear and Free the VUsers List } |
092 |
If Assigned(fVirtualUsers) then |
093 |
Begin |
094 |
With fVirtualUsers . LockList do |
095 |
Try |
096 |
for I := 0 to Count - 1 do |
097 |
TVirtualUser(Items[I]).Free; |
098 |
Clear; |
099 |
Finally |
100 |
fVirtualUsers . UnLockList; |
101 |
End ; |
102 |
FreeAndNil(fVirtualUsers); |
103 |
End ; |
104 |
Finally |
105 |
inherited ; |
106 |
End ; |
107 |
end ; |
108 |
|
109 |
function TVUManager . FindVU(ID: Cardinal ): TVirtualUser; |
110 |
Var |
111 |
I : Cardinal ; |
112 |
begin |
113 |
Result := NIL ; |
114 |
With fVirtualUsers . LockList do |
115 |
Try |
116 |
for I := 0 to Count - 1 do |
117 |
Begin |
118 |
if TVirtualUser(Items[I]).VUserID=ID then |
119 |
Begin |
120 |
Result := TVirtualUser(Items[I]); |
121 |
Break; |
122 |
End ; |
123 |
End ; |
124 |
Finally |
125 |
fVirtualUsers . UnlockList; |
126 |
End ; |
127 |
end ; |
128 |
|
129 |
function TVUManager . AddVUser(ID: Cardinal ): Boolean ; |
130 |
Var |
131 |
VU : TVirtualUser; |
132 |
begin |
133 |
Result := False ; |
134 |
With fVirtualUsers . LockList do |
135 |
Try |
136 |
VU := TVirtualUser . Create( NIL , ID); |
137 |
Add( VU ); |
138 |
Result := True ; |
139 |
Finally |
140 |
fVirtualUsers . UnlockList; |
141 |
End ; end ; |
142 |
|
143 |
function TVUManager . RemoveVUser(ID: Cardinal ): Boolean ; |
144 |
Var |
145 |
I : Cardinal ; |
146 |
VU : TVirtualUser; |
147 |
begin |
148 |
Result := False ; |
149 |
With fVirtualUsers . LockList do |
150 |
Try |
151 |
VU := NIL ; |
152 |
for I := 0 to Count - 1 do |
153 |
if TVirtualUser(Items[I]).VUserID = ID then |
154 |
Begin |
155 |
VU := TVirtualUser(Items[I]); |
156 |
Delete(I); |
157 |
Break; |
158 |
End ; |
159 |
Finally |
160 |
fVirtualUsers . UnlockList; |
161 |
if Assigned(VU) then |
162 |
Begin |
163 |
FreeAndNil(VU); |
164 |
Result := True ; |
165 |
End ; |
166 |
End ; |
167 |
end ; |
168 |
|
169 |
// END OF SOURCE |
170 |
End . |
An short explanation of what’s going on …
The main project file contains the functions that are exported by the DLL. These are available to the LR script. These in turn call methods from the VUManager object where the actual magic of the DLL will happen.
The VUManager has several useful methods (Find/Add/Remove vuser) and then the TVirtualUser object contains the actual methods that perform VUser specific stuff. To extend the DLL one can add methods to the TVirtualUser object and create a small Export interface in the main project file for the method (see the dll_TrimStr() for more details).
The dll_GetVersion() and dll_GetCopyright() functions are there only to provide the script with details of the DLL, and are not mandatory to use when a script uses the DLL.
The dll_Initialize() function
This function is responsible for allocating an instance of a VUser in the VUManager object. It accepts the VUserID that is a unique identifier across all running vusers in a test.
The dll_Finalize() function
The finalize function deallocates (or frees) the VUser instance in the VUManager object. It accepts the VUserID. After this call any dll functions that depend on a valid VUserID will fail with the corresponding error code.
The dll_TrimStr() function
The TrimStr function demonstrates how to “read” a string sent from LoadRunner, manipulate it and then return it to LoadRunner. The PAnsiChar is important here to make Delphi handle the string as an 8-bit null-terminated string, instead of a Unicode string.
How LoadRunner uses the DLL (in theory)
As soon as the MMDRV.EXE process loads the DLL, the DLL creates the VUManager object. This object is responsible for “Managing” the actual VUsers we are going to be servicing. Each VUser Thread also “loads” the DLL but in reality it only gets a copy of it in memory (this is not 100% accurate but good enough for now).
The actual LR script then uses the dll_Initialize(VUserID) function to initialize a VUser in the VUManager, that will allocate an TVirtualUser object for it, with the given ID. This way we can “keep track” of the individual VUsers later when needed. The dll_TrimStr() function takes the ID as the first parameter to identify the VUser.
A LoadRunner script using the DLL
The vuser_init() action:
01 |
int VuserID; // Script Global Variable |
02 |
vuser_init() |
03 |
{ |
04 |
int ret; |
05 |
|
06 |
// Get VUserID (Needs to be created as parameter) |
07 |
VUserID = atoi (lr_eval_string( "{VUserID" )); |
08 |
|
09 |
// Load the DLL |
10 |
ret = lr_load_dll( "MyDLL.dll" ); |
11 |
if (!ret==0) |
12 |
{ |
13 |
lr_error_message( "Could not load DLL" ); |
14 |
lr_abort; |
15 |
} |
16 |
|
17 |
// Initialize the VUser object inside the DLL |
18 |
dll_Initialize( VUserID ); |
19 |
|
20 |
return 0; |
21 |
} |
The Action() action:
01 |
Action() |
02 |
{ |
03 |
char buf[1024]; |
04 |
|
05 |
// Trim a string |
06 |
dll_TrimStr( VUserID, " this string will be trimmed!! " , buf, sizeof (buf) ); |
07 |
|
08 |
lr_output_message( "Trimmed String='%s'" , buf); |
09 |
|
10 |
return 0; |
11 |
} |
The vuser_end() action:
1 |
vuser_end() |
2 |
{ |
3 |
// Finalize the VUser (free the VUser object inside the DLL) |
4 |
dll_Finalize( VUserID ); |
5 |
|
6 |
return 0; |
7 |
} |
I hope this brief introduction to DLL writing for LoadRunner is of help to those who seek information regarding this subject.
Enjoy!