Version 13 of Yet another dll caller

Updated 2004-08-24 11:14:32

NJG August 24, 2004

You may well ask why bother with developing a new dll caller. Especially that originally I had wanted to use it only for accessing the Windows 2000 API. The short answer: ffidl cannot handle output pointer arguments to functions (or I overlooked something) and twapi (at the present stage of development) has a way too short Windows2000 API repertoire. Moreover, I deemed easier to write a new extension than patch the ffidl source (which I had contemplated for some time). Although in its present form the extension (bearing the rather unimaginative name dll) is a dll caller it would be fairly easy to turn it into an so caller for the Linux environment.

The sources and the windows binary may be downloaded from here [L1 ]

The following piece of script retrieves the names and positions of the desktop icons as well as the index of their images.

The package has a tcl and a binary component and can be activated by sourcing the former.

 #Store dll.dll alongside dll_tcl.tcl
 source $PackageDirectory/dll_tcl.tcl

When loaded the package creates the ::dll namespace and defines a number of commands within it. Of these ::dll::load is used for loading dynamic link libraries.

The first argument is the name of the library to be loaded. The naming convention used in calling LoadLibrary applies [L2 ]. The command also creates a new namespace for the loaded library the name of which is either [file rootname [file tail »name«]]] (in case of a single argument) or the one given in the optional -> name specification.

 ::dll::load user32 -> u
 ::dll::load kernel32 -> k

In the library namespace the loader defines a tcl command that can be used for assigning new tcl commands to the functions in the library.

 ::u::cmd "int GetDesktopWindow()"
 ::u::cmd "int FindWindowA(char *, char *)"
 ::u::cmd "int FindWindowExA(int, int, char *, char *)"
 ::u::cmd "int GetWindowThreadProcessId(int, int *)"
 ::u::cmd "int SendMessageA(int, int, int, int)"
 ::k::cmd "int OpenProcess(int, int, int)"
 ::k::cmd "int CloseHandle(int)"
 ::k::cmd "int VirtualAllocEx(int, int, int, int, int)"
 ::k::cmd "int VirtualFreeEx(int, int, int, int)"

(The declaration of the corresponding API functions can be found starting at [L3 ].)

As can be seen above, argument specifications follow a C-ish syntax. Base types are: void, char, uchar, short, ushort, int, uint, int64, uint64, float, double, pointer specs are * and **. Return type should always be specified, no return value by void.

By default the tcl command is created in the namespace of the library and with the same name as the corresponding library function. This can be overridden by the optional -> name specification. The first of the two following lines creates the command WPM in the global namespace for the WriteProcessMemory function in the kernel32.dll library; the second creates ::k::RPM for ReadProcessMemory.

 ::k::cmd "int WriteProcessMemory(int, int, void *, int, int *)" -> ::WPM
 ::k::cmd "int ReadProcessMemory(int, int, void *, int, int *)" -> RPM

Now we start using the armoury.

 set w1 [::u::FindWindowA "Progman" "Program Manager"]
 set w2 [::u::FindWindowExA $w1 0 SHELLDLL_DefView ""]
 set w3 [::u::FindWindowExA $w2 0 SysListView32 ""]

We now have the handle to the listview of the desktop the elements of which correspond to the desktop icons. So with a message sent to the window we ask how many of them are there.

 #  LVM_GETITEMCOUNT = LVM_FIRST(=0x1000)+4 (=4100)
 set iNum [::u::SendMessageA $w3 4100 0 0]

This particular message contains only constants as parameters. When pointers to data structures are passed, however, they are interpreted in the address space of the target process. So we have to place the parameters there. As the preliminary step we acquire the process ID -- returned in the variable pid -- of the process running the desktop.

 set th [::u::GetWindowThreadProcessId $w3 pid]

This line also shows that a pointer parameter requires the name of the variable as a command argument. The pointer passed to the function points to the internal representation of the variable's object thus it can both source and receive values for the function. If the variable does not exist it is created. Instead of a variable name the integer value 0 can be used as a NULL pointer.

We proceed by getting a handle on the process and then allocating 1000 bytes of storage in its address space for our purposes.

 # PROCESS_VM_OPERATION(=0x0008) | PROCESS_VM_READ(=0x0010) | PROCESS_VM_WRITE(=0x0020) (=56)
 set hnd [::k::OpenProcess 56 1 $pid]
 # AlocationType: MEM_COMMIT (=0x1000)
 # Protect: PAGE_READWRITE (=0x04)
 set addr [::k::VirtualAllocEx $hnd 0 1000 4096 4]

In the sequel we will need some memory space in our address space as well.

 set buff1 [::dll::buffer 1000]
 set buff2 [::dll::buffer 1000]

The ::dll::buffer command creates a bytearray value (as e.g. binary format does) of all 0's. As a warm up we clear part of our memory allocated in the desktop process.

 WPM $hnd $addr buff2 1000 0

We next iterate over the elements of the desktop's listview. In each iteration we receive the corresponding icon parameters into a LISTVIEW structure [L4 ].

 set nStart 96
 for {set i 0} {$i < $iNum} {incr i} {
    set lvitem [binary format iiiiiiiiiiiii 3 $i 0 0 0 [expr $addr + $nStart] 128 0 0 0 0 0 0]
    WPM $hnd [expr $addr + 8] lvitem 52 0
    # LVM_GETITEMPOSITION = LVM_FIRST+16 (=4112)
    ::u::SendMessageA $w3 4112 $i $addr
    # LVM_GETITEMA = LVM_FIRST+5 (=4101)
    ::u::SendMessageA $w3 4101 $i [expr $addr + 8]

The next call illustrates another rule. When a function parameter refers to memory filled during the function call, the corresponding parameter specification must be void * and a bytearray of suitable size should be provided.

    ::k::RPM $hnd $addr buff1 200 0
    binary scan $buff1 ii posX posY
    binary scan [string range $buff1 36 39] i imageIdx
    set txt [string range $buff1 $nStart end]
    set txt [string range $txt 0 [expr [string first "\x00" $txt] - 1]]
 puts "$i -- $posX $posY\t: @$imageIdx\t> $txt" 
 }

At this point we bail out cleanly.

 # MEM_RELEASE (=0x8000)
 ::k::VirtualFreeEx $hnd $addr 0 32768
 ::k::CloseHandle $hnd

We are not finished yet however. The package provides four more commands:

  • ::dll::memrd address ?length? - Copies length memory bytes from address into the returned bytearray value. If length is missing the first four octets at address are assumed to contain the length. Negative length specifies a string copy and the return value is an ordinary tcl string.
  • ::dll::memwr name address - Copies the bytearray value of the variable name to memory starting at address.
  • ::dll::ref name - Name should refer to a bytearray variable, the call returns the starting memory address of the array (suitable for ::dll::memrd without length specification). The reference count of the variable's object is incremented.
  • ::dll::deref name - Name should refer to a bytearray variable. The reference count of the variable's object is decremented.

These commands are included for a couple of reasons:

  • They are needed for constructing the necessary input/output structures for ** (pointer-to-pointer) parameter specifications
  • When pointer is specified as the return type only the value of the pointer is returned. Further extraction of values should be done in tcl.

Finally

  • ::dll::error is the error code returned by windows after the windows API function call.
  • ::dll::alias is an array that has an element for all created commands containing the default name that would have been used if -> name would not have been specified.
  • ::dll::entry is an array that holds as bytearray values the internal specifications for the created commands (of not much use outside of debugging).
  • <namespace>::handle holds the handle to the loaded dynamic library.
  • <namespace>::alias gives the name of the dynmic library loaded for namespace.

Use it at your own risk. Works for me though.