Performance Testing @ the Frontline

A hidden world where small things make a big difference

Creating a DLL with Delphi for LoadRunner

Posted by Kim on Tuesday, April 13, 2010

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):

  library Project1;

  uses
    SysUtils,
    Classes;

  {$R *.res}

  begin
  end.

Now copy & paste the following instead of the above:

library MyDLL;

uses
  SysUtils, Classes, Windows,
  VUManager in 'VUManager.pas';

Const
  { Version Constants }
  DLLVer_Major                 = 1;
  DLLVer_Minor                 = 0;
  DLLVer_Copyright             = 'Copyright (C) by Celarius Oy';

  { DLL Errors - Negatives are errors }
  ERR_None                     =  0;
  ERR_Unknown                  = -1;
  ERR_Unknown_Object           = -2;
  ERR_Insufficient_Buffer      = -3;

////////////////////////////////////////////////////////////////////////////////

Var
  VUMgr : TVUManager;

{$R *.res}

////////////////////////////////////////////////////////////////////////////////

function dll_Initialize( VUserID: Cardinal ): Integer; stdcall;
// Initialize DLL, Adds the Vuser with the ID
//
// Returns
//    Integer    The Error code
begin
     If VUMgr.AddVUser( VUserID ) then
        Result := ERR_None
     Else
        Result := ERR_Unknown_Object;
end;

////////////////////////////////////////////////////////////////////////////////

function dll_Finalize( VUserID: Cardinal ): Integer; stdcall;
// Finalize DLL, removes the VUser with the ID
//
// Returns
//    Integer    Error code
begin
     If VUMgr.RemoveVUser( VUserID ) then
        Result := ERR_None
     Else
        Result := ERR_Unknown_Object;
end;

////////////////////////////////////////////////////////////////////////////////

function dll_GetVersion: Integer; stdcall;
// Get Library version
//
// Returns
//    Integer    Version Number
begin
     Result := (DLLVer_Major SHL 16) + DLLVer_Minor;
end;

////////////////////////////////////////////////////////////////////////////////

function dll_GetCopyright( DestStr: PAnsiChar; DestSize: Integer): Integer; stdcall;
// Get Library Copyright String
//
// Returns
//    Integer    Version Number
begin
     if (DestSize>Length(DLLVer_Copyright)) then
     Begin
          StrCopy( DestStr, PAnsiChar(DLLVer_Copyright) );
          Result := ERR_None;
     End Else Result := ERR_Insufficient_Buffer;
end;

////////////////////////////////////////////////////////////////////////////////

function dll_TrimStr(ID:Cardinal; SourceStr: PAnsiChar; DestStr: PAnsiChar; DestSize: Integer): Integer; stdcall;
// Trims a String (removes special chars in beginning/end)
//
// Returns
//    Integer    Error Code
Var
   VU : TVirtualUser;
   S  : AnsiString;
begin
     { Find the Virtual User }
     VU := VUMgr.FindVU(ID);
     if Assigned(VU) then
     Begin
          S := VU.TrimString( SourceStr ); // process the string

          if (DestSize>Length(S)) then
          Begin
               StrCopy( DestStr, PAnsiChar(S) );
               Result := ERR_None;
          End Else Result := ERR_Insufficient_Buffer;

     End Else Result := ERR_Unknown_Object;
end;

////////////////////////////////////////////////////////////////////////////////

{ Export functions }
exports
  { General Functions }
  dll_Initialize       name 'dll_Initialize',
  dll_Finalize         name 'dll_Finalize',
  dll_GetVersion       name 'dll_GetVersion',
  dll_GetCopyright     name 'dll_GetCopyright',
  dll_TrimStr          name 'dll_TrimStr'
  ;

////////////////////////////////////////////////////////////////////////////////

procedure DLLEntryProc(EntryCode: integer);
//
// DLL Entry/Exit, Process and Thread Attach/Detach procedures }
//
Var
   Buf           : Array[0..MAX_PATH] of Char;
   LoaderProcess : String;
   DLLPath       : String;
begin
     Try
        Case EntryCode of
             { The DLL has just been loaded with LoadLibrary()               }
             { either by the main process, or by a vuser - we do not know    }
             { which one                                                     }
             DLL_PROCESS_ATTACH:
             Begin
                  { Get the Path where the DLL is }
                  GetModuleFileName(HInstance, buf, Length(buf)) ;
                  DLLPath := ExtractFilePath(StrPas(buf));
                  { Get the name of the module that loaded us }
                  GetModuleFileName(HInstance, buf, Length(buf)) ;
                  LoaderProcess := StrPas(buf);
                  { Create handler }
                  VUMgr:= TVUManager.Create( NIL );
             End;

             { The Process Detached the DLL - Once per MMDRV Process }
             { Application called Unload - Perform Cleanup }
             DLL_PROCESS_DETACH:
             Begin
                  If Assigned(VUMgr) then FreeAndNil(VUMgr);
             End;

             { A Thread has just loaded the DLL }
             DLL_THREAD_ATTACH:
             Begin
                  { This occures when a VUser calls lr_load_dll() }
             End;

             { A Thread has just unloaded the DLL }
             DLL_THREAD_DETACH:
             Begin
                  { This occures when a VUser exits }
             End;
        End;
     Except
        // TODO: Write an Exception Handler
        On E: Exception do ;
     End;
end;

////////////////////////////////////////////////////////////////////////////////
//
// DLL Initialization - When DLL is loaded the first time by any process }
//
////////////////////////////////////////////////////////////////////////////////
begin
{$IFDEF Debug}
          { True means Delphi checks & reports memleaks }
          ReportMemoryLeaksOnShutdown := True;
{$ENDIF}
          { Make the Memory-Manager aware that this DLL is used in Thread-environments }
          IsMultiThread := True;
          VUMgr:= NIL;

          { If we have not already set te DLLProc, do it }
          if NOT Assigned(DllProc) then
          begin
               DLLProc := @DLLEntryProc;         // install custom exit procedure
               DLLEntryProc(DLL_PROCESS_ATTACH);
          end;
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.

unit VUManager;

Interface

Uses SysUtils, Classes, Windows;

Type
  { Forward Declarations }
  TVirtualUser = class;
  TVUManager = class;

////////////////////////////////////////////////////////////////////////////////

  TVirtualUser = class(TComponent)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
    VUserID               : Cardinal;
    Constructor Create(AOwner:TComponent; ID:Cardinal); ReIntroduce;
    Destructor Destroy; Override;
    Function TrimString(InStr: AnsiString): AnsiString;
  end;

////////////////////////////////////////////////////////////////////////////////

  TVUManager = class(TComponent)
  private
    { Private declarations }
    fVirtualUsers          : TThreadList;
  protected
    { Protected declarations }
  public
    { Public declarations }
    Constructor Create(AOwner:TComponent); Override;
    Destructor Destroy; Override;
    { Methods }
    Function FindVU(ID:Cardinal):TVirtualUser;
    Function AddVUser(ID: Cardinal): Boolean;
    Function RemoveVUser(ID: Cardinal): Boolean;
  end;

////////////////////////////////////////////////////////////////////////////////

Implementation

////////////////////////////////////////////////////////////////////////////////

{ TVirtualUser }

constructor TVirtualUser.Create(AOwner: TComponent; ID:Cardinal);
begin
     inherited Create(AOwner);
     VUserID := ID;
end;

destructor TVirtualUser.Destroy;
begin
     Try
        { Cleanup anything that is allocated by the VUser }
     Finally
        inherited;
     End;
end;

function TVirtualUser.TrimString(InStr: AnsiString): AnsiString;
begin
     Result := Trim(InStr);
end;

////////////////////////////////////////////////////////////////////////////////

{ TVUManager }

constructor TVUManager.Create(AOwner: TComponent);
begin
     Try
        inherited;
        fVirtualUsers  := TThreadList.Create();
     Finally
     End;
end;

destructor TVUManager.Destroy;
Var
   I : Integer;
begin
     Try
        { Clear and Free the VUsers List }
        If Assigned(fVirtualUsers) then
        Begin
             With fVirtualUsers.LockList do
             Try
                for I := 0 to Count - 1 do
                    TVirtualUser(Items[I]).Free;
                Clear;
             Finally
                fVirtualUsers.UnLockList;
             End;
             FreeAndNil(fVirtualUsers);
        End;
     Finally
        inherited;
     End;
end;

function TVUManager.FindVU(ID: Cardinal): TVirtualUser;
Var
   I : Cardinal;
begin
     Result := NIL;
     With fVirtualUsers.LockList do
     Try
        for I := 0 to Count - 1 do
        Begin
             if TVirtualUser(Items[I]).VUserID=ID then
             Begin
                  Result := TVirtualUser(Items[I]);
                  Break;
             End;
        End;
     Finally
        fVirtualUsers.UnlockList;
     End;
end;

function TVUManager.AddVUser(ID: Cardinal): Boolean;
Var
   VU : TVirtualUser;
begin
     Result := False;
     With fVirtualUsers.LockList do
     Try
        VU := TVirtualUser.Create(NIL, ID);
        Add( VU );
        Result := True;
     Finally
        fVirtualUsers.UnlockList;
     End;end;

function TVUManager.RemoveVUser(ID: Cardinal): Boolean;
Var
   I : Cardinal;
   VU : TVirtualUser;
begin
     Result := False;
     With fVirtualUsers.LockList do
     Try
        VU := NIL;
        for I := 0 to Count - 1 do
            if TVirtualUser(Items[I]).VUserID = ID then
            Begin
                 VU := TVirtualUser(Items[I]);
                 Delete(I);
                 Break;
            End;
     Finally
        fVirtualUsers.UnlockList;
        if Assigned(VU) then
        Begin
             FreeAndNil(VU);
             Result := True;
        End;
     End;
end;

// END OF SOURCE
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:

int VuserID; // Script Global Variable
vuser_init()
{
	int ret;

	// Get VUserID (Needs to be created as parameter)
    VUserID = atoi(lr_eval_string("{VUserID"));

	// Load the DLL
    ret = lr_load_dll("MyDLL.dll");
	if (!ret==0)
	{
		lr_error_message("Could not load DLL");
		lr_abort;
	}

	// Initialize the VUser object inside the DLL
    dll_Initialize( VUserID );

	return 0;
}

The Action() action:

Action()
{
	char buf[1024];

	// Trim a string
	dll_TrimStr( VUserID, "  this string will be trimmed!!  ", buf, sizeof(buf) );

	lr_output_message("Trimmed String='%s'", buf);

	return 0;
}

The vuser_end() action:

vuser_end()
{
	// Finalize the VUser (free the VUser object inside the DLL)
    dll_Finalize( VUserID );

	return 0;
}

I hope this brief introduction to DLL writing for LoadRunner is of help to those who seek information regarding this subject.

Enjoy!

Advertisements

5 Responses to “Creating a DLL with Delphi for LoadRunner”

  1. Stanton Lofts said

    There’s good info here. I did a search on Google, Keep up the good work mate!

  2. Random said

    Hello Kim, have you implemented and executed Windows API in LoadRunner. Particularly Windows sockets. Thank you.

    • Kim said

      I’ve used some windows API calls yes. The easiest way to implement them is to load the appropriate DLL and call the functions directly. In most cases it’s no point to write your own DLL to call another DLL’s functions šŸ™‚

      As for socket functions, no I have not used those directly from the scripts. These tend to be more than just a few calls (protocols) so embedding them into a custom DLL is often easier.

  3. Random said

    Hello Kim, thank you for your reply. Thank you for your other pointers at the top. Keep it going!

  4. uk directories…

    […]Creating a DLL with Delphi for LoadRunner « Performance Testing @ the Frontline[…]…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: