Page 1 of 2

Access global array in main app from DLL

Posted: Tue Nov 06, 2007 1:23 pm
by PB
I've got a DLL that I've written, that needs to access a global array in my
main app. The main app calls the DLL to work on some windows, but only
windows that the user specifies. These window captions are stored in the
global array. But I can't get my DLL to recognise that array. If I try, the
variables are just null. What can I do?

As an example: I have a global variable in my app called "fg", and the
same thing in my DLL, but they both hold different values. It's like the
two variables are totally separate, even though both are named "fg"
and both are marked as global.

I don't have an example of my code to post yet (too big) but if there's
something obvious I'm missing, then just give me a hint and hopefully
I won't need to make a snippet for you. :)

Posted: Tue Nov 06, 2007 1:29 pm
by srod
Yes, the dll will use it's own heap etc. and thus will not recognise the global array from your calling program.

Why not pass the address of the array to the dll?

E.g. (main program)

Code: Select all

Global Dim MyArray.l(100)
CallFunctionFast(*ptrDllFunction, MyArray())
Better still, if using prototypes, simply pass the array as a parameter etc.

Posted: Tue Nov 06, 2007 1:37 pm
by PB
I'm not sure I follow you. :(

The main app creates the array, and the DLL is what acts on it. Like this:

Main app:

Code: Select all

Global nw ; Number of windows
Global Dim win$(2)
win$(1)="Calculator"
win$(2)="Untitled - Notepad"
nw=2
DLL:

Code: Select all

For a=1 To nw
  If FindWindow_("",win$(a))
    ; Do something
  EndIf
Next
That's basically what I'm trying to do. :)

Posted: Tue Nov 06, 2007 1:45 pm
by srod
The dll will NOT recognise any global variables defined in the calling program.

This is because it uses it's own memory for such things. More importantly, in order for the dll to be able to recognise global variables in the calling application, it would of course require re-linking when it was loaded by Windows. Now this is not possible because (unlike a static library) dll's are fully linked by the linker at the time they are created. This cannot be changed by the loader.

Think about it, if a dll had access to the global variables in the calling program, this would actually to some extent negate the whole point of using dll's in the first place! They would become an extension of the calling program and this is really better suited to static libraries.

You will thus need to pass the address of the array to your dll functions as I explained above! :)

Posted: Tue Nov 06, 2007 2:01 pm
by PB
> pass the address of the array to your dll functions

The main app never passes anything to the DLL though. The DLL acts alone.
In fact, I'd put the DLL code in my main app if I could, but then the tasks it
performs don't work, as they NEED to be in a DLL to work. That's the issue.

Posted: Tue Nov 06, 2007 2:03 pm
by srod
I don't understand. Just alter your code so that it passes the address of the array and alter the dll functions as appropriate etc. Unless I am missing something here?

Alternatively add an init_ function in which you pass the address of the array and the dll records it in a global array of it's own etc.

Posted: Tue Nov 06, 2007 2:07 pm
by PB
The DLL is a hook, which is why it has to be separate from the main app.
It doesn't work as a hook in the main app.

[Edit] Okay, here's a snippet that shows the concepts of what I want to do.
In this example, I want to change calc.exe's window caption from "Calculator"
to "PureBasic" BEFORE its window is activated. This hook achieves this nicely.
In my expanded app, I want to do something similar with an array of windows.

Main app:

Code: Select all

If OpenWindow(0,300,300,200,100,"Hook example",#PB_Window_SystemMenu)
  If OpenLibrary(9,"CalcRename.dll")
    dllInstance=LibraryID(9) : dllFunction=GetFunction(9,"CalcRename")
    HookID=SetWindowsHookEx_(#WH_CALLWNDPROC,dllFunction,dllInstance,0)
  EndIf
  Repeat : Until WaitWindowEvent()=#PB_Event_CloseWindow
  If HookID : UnhookWindowsHookEx_(HookID) : CloseLibrary(9) : EndIf
EndIf
DLL:

Code: Select all

ProcedureDLL CalcRename(nCode.l,wParam.l,*lParam.CWPSTRUCT)
  fg=*lParam\hwnd
  If *lParam\message=#WM_ACTIVATE
    fg$=Space(999) : GetWindowText_(fg,fg$,999)
    If fg$="Calculator"
      SetWindowText_(fg,"PureBasic")
    EndIf
  EndIf
  ProcedureReturn CallNextHookEx_(@CalcRename(),nCode,wParam,*lParam)
EndProcedure
Compile the DLL code above into a DLL file called "CalcRename.dll" and
put it in the same folder as a compiled exe from the Main code above.
Then run the main exe, and start calc.exe -- you'll see it's caption says
"PureBasic" instead of "Calculator".

I can't do this without a DLL. I need an array of titles to be known by the
DLL, but how? I can't read them from disk because the DLL is looped, so
disk access would be constant. See my predicament? :)

My aim is to get the DLL routine to do something like this:

Code: Select all

ProcedureDLL CalcRename(nCode.l,wParam.l,*lParam.CWPSTRUCT)
  fg=*lParam\hwnd
  If *lParam\message=#WM_ACTIVATE
    fg$=Space(999) : GetWindowText_(fg,fg$,999)
    For a=1 to nw
      If fg$=win$(a)
        SetWindowText_(fg,fg$+" wow!")
      EndIf
    Next
  EndIf
  ProcedureReturn CallNextHookEx_(@CalcRename(),nCode,wParam,*lParam)
EndProcedure

Posted: Tue Nov 06, 2007 2:34 pm
by srod
I don't see an array in that code.

Anyhow here's one way of passing a string array to a procedurel. I use this method only because the procedure would of course be in a dll. In fact there used to be an easier way, but there seems to be a bug with the latest version of PB which I'm looking at.

Code: Select all

Dim Name$(10)
Name$(3)="Hello!"

Procedure CopyArray(*addressOfArray)
  Protected *ptrString.String
  ;Set this pointer to point at the 4th (index 3) element of the string array.
    *ptrString = *addressOfArray + 3*SizeOf(Long)
    Debug *ptrString\s
EndProcedure

CopyArray(Name$())

Posted: Tue Nov 06, 2007 2:36 pm
by PB
> I don't see an array in that code

Of course not; it's just to show the concept and to show why I can't just do
it all in my main code. I edited my post, so look up again and you'll see what
I want to do.

I'll try your example of CopyArray in a moment. :)

Posted: Tue Nov 06, 2007 2:40 pm
by srod
Right I now see why you cannot pass the address of the global array.

Okay you will need an initialisation function in the dll which you will need to call before installing the hook.

Something like :

Code: Select all

;DLL CODE.

Global _dllAddressOfArray

ProcedureDLL _InitHOOK(*AddressOfGlobalArray)
  _dllAddressOfArray = *AddressOfGlobalArray
EndProcedure


ProcedureDLL CalcRename(nCode.l,wParam.l,*lParam.CWPSTRUCT) 

...etc.

EndProcedure
Now, in you main application, make sure you call _InitHOOK() before installing the hook and passing the address of your array as parameter.

Then in your CalcRename() function you can use a string pointer in combination with the global variable _dllAddressOfArray to access the elements of the array as I did with my first example above.


**EDIT : just tested with a dll and it works fine.

Posted: Sat Nov 10, 2007 2:09 pm
by QuimV
Hi,

I've tried to test that solution and it doesn't run.

Here it is the DLL code:

Code: Select all

;
Global _dllAddressOfArray.l 
;
ProcedureDLL _InitHOOK(*AddressOfGlobalArray) 
  Shared _dllAddressOfArray.l 
  _dllAddressOfArray.l = *AddressOfGlobalArray 
EndProcedure 
;
ProcedureDLL CalcRename(nCode.l,wParam.l,*lParam.CWPSTRUCT) 
  Protected *ptrString.String 
  Protected fg.l, fg$, b.s
  
  fg=*lParam\hwnd 
  If *lParam\message=#WM_ACTIVATE 
    fg$=Space(999)
    GetWindowText_(fg,fg$,999) 

    ;Set this pointer to point at the 4th (index 3) element of the string array. 
    *ptrString = _dllAddressOfArray + 3*SizeOf(Long) 
    b.s = (*ptrString\s)

    If fg$ = b.s 
      SetWindowText_(fg,"PureBasic") 
    EndIf 
  EndIf 
  ProcedureReturn CallNextHookEx_(@CalcRename(),nCode,wParam,*lParam) 
EndProcedure 
and here it is the test code:

Code: Select all

Dim Name$(10) 
Name$(3)="Calculadora" 

If OpenWindow(0,300,300,200,100,"Hook example",#PB_Window_SystemMenu) 
  If OpenLibrary(9,"CalcRename.dll") 
    dllInstance=LibraryID(9)
    dllFunction=GetFunction(9,"_InitHOOK") 
    CallFunctionFast(dllFunction,Name$())    
    dllFunction=GetFunction(9,"CalcRename") 
    HookID=SetWindowsHookEx_(#WH_CALLWNDPROC,dllFunction,dllInstance,0) 
  EndIf 
  Repeat : Until WaitWindowEvent()=#PB_Event_CloseWindow 
  If HookID : UnhookWindowsHookEx_(HookID) : CloseLibrary(9) : EndIf 
EndIf 
Why it doesn't run properly?

Any idea?

Thanks

Posted: Sat Nov 10, 2007 2:35 pm
by srod
Ah yes, I didn't test with the hook.

The thing is that the dll is being injected into all running processes and each will then have it's own copy of the global variable; only one of which has been initialised.

Right, need to give this some thought.

Posted: Sat Nov 10, 2007 3:54 pm
by srod
God I'm an idiot sometimes! :)

Of course this will not work, the dll is being injected into every process running. Now the strings in the global array are only available to the process creating the array - global or not! What a dimwitted bumpkin I am!

Doh!

@PB, QuimV - I can use shared memory okay to pass data so that every process using the hook can access the data. The question is, do you wish me to write the entire array to this shared memory? This way each process will then simply read a copy of the array etc.

The problem with this is that any changes you make to the original array after setting the hook will not be picked up by the other processes who will continue to use the original array etc. It's up to you, I don't mind doing this as I have the shared memory aspect already working.

Posted: Sat Nov 10, 2007 4:20 pm
by srod
Okay, this interested me enough to proceed anyhow.

The problem, as explained above, is that the dll is being injected into every process running. Now the strings in the global array are only available to the process creating the array - global or not and thus all other processes into which the dll is injected will not be able to access the strings. In fact, all hell breaks loose when you even try!

One solution is to create a chunk of 'shared memory' (accessible by all instances of the dll) and physically copy the string array into it (this is done before the hook is installed). Then, when the global hook is installed into all existing processes, use the AttachProcess() dll function to load a copy of the original array into an array useable by the particular process into which the dll is injected. This means that each running process will have a physical (and separate) copy of the array to which it alone has access.

The dll code: (testdll.dll)

Code: Select all

#FileMapSize = 10000 ;Enusre this is big enough for the string array.
Global hMapObject, hMapView
Global Dim Name$(0)

#_FileMappingObject = "dllSharedMem"

;The following is called once BEFORE the global hook is installed and copies the string array
;to shared memory.
;All other processes into which this dll is injected will retrieve the strings in the AttachProcess()
;procedure.
;Returns non-zero if everything is okay.
ProcedureDLL.l _InitHOOK(*AddressOfGlobalArray, arrayUpperBound) 
  Protected i, *ptrString.String, ptr
  ;Create a named file mapping object.
    hMapObject = CreateFileMapping_(-1, 0, #PAGE_READWRITE, 0, #FileMapSize, #_FileMappingObject)
  If hMapObject
    hMapView = MapViewOfFile_(hMapObject,#FILE_MAP_WRITE,0,0,0)
    If hMapView
      ;Copy the upper bound to the memory mapped file.
        PokeL(hMapView, arrayUpperBound)
      ;Now copy the strings.
        ptr = hMapView+SizeOf(Long)
        *ptrString = *AddressOfGlobalArray
        Redim Name$(arrayUpperBound)
        For i = 0 To arrayUpperBound
          Name$(i) = *ptrString\s
          PokeS(ptr, *ptrString\s, -1, #PB_UTF8)
          ptr+StringByteLength(*ptrString\s, #PB_UTF8)+1
          *ptrString+SizeOf(Long)
        Next
      ProcedureReturn #True
    EndIf
  EndIf
  ProcedureReturn #False ;Error creating the file mapping object.
EndProcedure 


;The following is called whenever the dll is attached to a new process.
;In all cases except the main application, we copy the string array from the shared memory
;into a string array accessible by the particular process into which the dll is injected.
ProcedureDLL AttachProcess(Instance)
  Protected i, *ptrString.String, ptr, arrayUpperBound
  ;Check that this is not the main application.
  If hMapObject = 0 ;The shared memory needs to be opened and mapped.
    hMapObject = CreateFileMapping_(-1, 0, #PAGE_READWRITE, 0, #FileMapSize, #_FileMappingObject)
    If hMapObject
      hMapView = MapViewOfFile_(hMapObject,#FILE_MAP_WRITE,0,0,0)
      If hMapView
        ;Copy the string array from shared memory.
          arrayUpperBound = PeekL(hMapView)
          Redim Name$(arrayUpperBound)
          ptr = hMapView+SizeOf(Long)
          For i = 0 To arrayUpperBound
            Name$(i) = PeekS(ptr, -1, #PB_UTF8)
            ptr+StringByteLength(Name$(i), #PB_UTF8)+1
          Next
        ;Now unmap the memory mapped file from this process as it is no longer required.
          UnmapViewOfFile_(hMapView)
          hMapView=0
      EndIf
      ;Delete the named file mapping object from this process as it is no longer required.
        CloseHandle_(hMapObject)
        hMapObject = 0
    EndIf
  EndIf
EndProcedure

;The following is called whenever the dll is detached from an existing process.
;We use it to unmap the memory mapped file and delete the file mapping object from the
;main application as it has already been removed from all other processes.
ProcedureDLL DetachProcess(Instance)
  If hMapView
    UnmapViewOfFile_(hMapView)
  EndIf
  If hMapObject
    CloseHandle_(hMapObject)
  EndIf
  Redim Name$(0)
EndProcedure

 
ProcedureDLL.l CalcRename(nCode.l,wParam.l,*lParam.CWPSTRUCT) 
  If *lParam\message=#WM_ACTIVATE 
    fg=*lParam\hwnd 
    fg$=Space(999) 
    GetWindowText_(fg,fg$,999) 
    If Name$(3) = fg$
        SetWindowText_(fg,"Ya boo sucks to you!") 
    EndIf
  EndIf
  ProcedureReturn CallNextHookEx_(@CalcRename(),nCode,wParam,*lParam)
EndProcedure 
Test code :

Code: Select all

Dim Name$(4) 
Name$(3)="jaPBe V3" ;Change this to the window title of some existing process on your system.

If OpenWindow(0,300,300,200,100,"Hook example",#PB_Window_SystemMenu) 
  If OpenLibrary(9,"testdll.dll") 
    dllInstance=LibraryID(9) 
    dllFunction=GetFunction(9,"_InitHOOK") 
    CallFunctionFast(dllFunction,Name$(), 4)    
    dllFunction=GetFunction(9,"CalcRename") 
    HookID=SetWindowsHookEx_(#WH_CALLWNDPROC,dllFunction,dllInstance,0) 
  EndIf 
  Repeat : Until WaitWindowEvent()=#PB_Event_CloseWindow 
  If HookID : UnhookWindowsHookEx_(HookID) : CloseLibrary(9) : EndIf 
EndIf 
On my system (running jaPBe), I run the program and click into the jaPBe window to see the title changed.

Warning, I haven't done much testing! :)

Also, as commented in the post above, it is not possible (at least with the code posted here) to update the array in the main application and have each process also update their copies of the array. Although you could get around this by using a user defined message since each instance of the dll is monitoring message queues anyhow! I'll leave this to someone else though.

Posted: Sat Nov 10, 2007 4:34 pm
by QuimV
:D
Thank a lot for your effort and your teachings.
You Are a super!