Version 27 of Yet another dll caller

Updated 2010-07-07 15:56:03 by AMG

daapp Source code available here: http://web.archive.org/web/20060314225233/http://cpluscsystems.axelero.net/cgi-bin/download.php?category=programming&collection=tcltk&item=dll.zip , but license is not specified.

NJG May 24, 2006

Please, visit Yet again dll caller, new developments are announced there.

APN Jan 21 2007, I was looking for the source for this and could not find it in the dll.zip download. Are there plans for the C source to be released?

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 each created command 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.


Peter Newman 7 January 2005: Nice work NJG. Much easier to call the Windows API with this, than with any of the alternatives I've tried (Swig, Ffidl and critcl).

But may I suggest you add global type to the top of your start-up script. Otherwise, if the start-up script isn't sourced at the global level, the type array becomes local, and the script breaks. Cheers.


escargo 20 Mar 2006 - I was looking at using dll to interface to the current version of AutoIt. The help file provides a convenient list of DLL entries, but mapping what dll needs to what the document says is provided leaves me with a bit of a puzzle.

Here are some examples:

 ///////////////////////////////////////////////////////////////////////////////
 // Exported functions
 ///////////////////////////////////////////////////////////////////////////////

 AU3_API void WINAPI AU3_Init(void);
 AU3_API long AU3_error(void);

 AU3_API long WINAPI AU3_AutoItSetOption(const char *szOption, long nValue);

 AU3_API void WINAPI AU3_BlockInput(long nFlag);

 AU3_API void WINAPI AU3_CDTray(const char *szDrive, const char *szAction);
 AU3_API void WINAPI AU3_ClipGet(char *szClip, int nBufSize);

First, I presume the AU3_API and WINAPI symbols are just going to be ignored (to dll they don't signify anything).

Next, the docs for dll don't say anything about what to do about const in the parameter list, and I didn't see any examples.

Finally, where the prototype has (for example) "char *szDrive", is that where the <namespace>::cmd would use something like this: <namespace>::cmd "void AU3_CDTray(char *, char *)"

There's also an interesting issue with some of the commands taking optional arguments. It's not at all clear how to do that with dll.

Any light you could shed on this would be welcome; once I have a new interface to AutoIt done, I'll release it back to the wiki.