Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MouseOverControl.cls file imported in Modules folder instead of the Class Modules folder. #28

Closed
NicoR45 opened this issue Sep 4, 2023 · 15 comments

Comments

@NicoR45
Copy link

NicoR45 commented Sep 4, 2023

Problem : in Microsoft 365 MSO (Version 2302 Build 16.0.16130.20690) 32-bit VBA Editor, when the MouseOverControl.cls file is imported, it is imported into the Modules folder instead of the Class Modules folder.

Solution : the issue appears to be line endings. When downloaded the MouseOverControl.cls file from Github, the line endings are UNIX-style (LF or \n) and from my local files they are Windows-style (CR + LF or \r\n). After switching the downloaded file with Windows-style line endings the file imported correctly as a Class.

Can you make the file download Windows-style please?

@cristianbuse
Copy link
Owner

Hi @NicoR45 ,

Thanks for raising the issue!

Presumably this is an issue when downloading the raw file, which I will make it Windows-style as requsted.

Can you please download the available zip and then import the MouseOverControl.cls module? Does it still import as a standard module or does it import properly as a class module?

Thanks!

@guwidoe
Copy link
Contributor

guwidoe commented Oct 12, 2023

I noticed the same behavior, using the zip file as you recommended indeed prevents this issue :)

@cristianbuse
Copy link
Owner

cristianbuse commented Oct 12, 2023

Thanks @guwidoe ,

I followed the stepes outlined here and it still doesn't want to update the line endings. Any idea what I am doing wrong?

@guwidoe
Copy link
Contributor

guwidoe commented Oct 12, 2023

I never had to deal with this issue before so unfortunately no.

@cristianbuse
Copy link
Owner

Hi @Greedquest ,

Apologies for contacting you here, but do you have any idea how to solve this. It seems whatever I do does not fix the line endings when downloading the raw file. Thanks!

@Greedquest
Copy link

Greedquest/ListObject-WithEvents#2

Is a workaround after cloning

then I believe

*.bas eol=crlf linguist-language=VBA
*.cls eol=crlf linguist-language=VBA
*.doccls eol=crlf linguist-language=VBA
*.twin linguist-language=VBA

Is what I use in my gitattributes although let me just check if it downloads correctly

@Greedquest
Copy link

Greedquest commented Oct 12, 2023

More info: GitHub will perform the normalisation when you download as zip, even if the files are stored with LF only on github

https://stackoverflow.com/a/76548787/6609896


Ok I'm re-reading and none of this is news to you. If you followed the steps to re-apply line endings in that help page you listed then I don't have any bright ideas I'm afraid. You could clone the repo, run that powerquery on it, commit changes. See in notepad++ whether the CRLF in gitattributes is actually being respected

@cristianbuse
Copy link
Owner

Thanks @Greedquest !

https://stackoverflow.com/a/76548787/6609896

Darn. Lost hope now.

I basically have:

*.bas text eol=crlf linguist-language=VBA
*.cls text eol=crlf linguist-language=VBA
*.frm text eol=crlf linguist-language=VBA

I only added the text word today but that did nothing. Also followed these steps which also did nothing.

I just downloaded one of your cls files and it downloads with lf (not crlf) so same issue.


You might be interested in this trick. It basically fixes the way the arguments are pushed on the stack for x64 by faking the delegate signature. This also solves it for SetTimer. The gain is that one can now step through the code in x64 whereas before that would have caused a crash. It still crashes when pressing Reset while in the scope of the callback but that's another issue.

@Greedquest
Copy link

Greedquest commented Oct 12, 2023

@cristianbuse Yeah I just checked my own files have only the LF - although that is laziness since I expect users to download my .xlam or clone my repo, not download raw files individually. But linefeed correction is not mandatory in git, it is to avoid merge conflicts if one dev is on UNIX, one is on Windows, you would get a conflict of linefeed characters when both push the same file on github. So Git normalises all to lf before pushing to ensure this isn't an issue. However you can disable that automatic behaviour to ensure crlf gets pushed to github surely?

git config --global core.autocrlf false

Then you commit all your crlfs to git. Also it seems if you double up in the gitattributes file you specify one linefeed style for OnCommit, one for OnCheckout. Ensure the OnCommit is crlf and that's what should end up on github IIUC.

https://stackoverflow.com/questions/10418975/how-to-change-line-ending-settings#:~:text=*%20text%20eol%3Dcrlf,is%20for%20commit.

But untested


I don't understand that trick - what is the magic number and what is the argument alignment issue it solves? I remember you mentioning it but it was over my head at the time? But I got my asm working! I have a lightweight COM object implementing IEnumVariant using a simple VTABLE of vba function pointers. Allocated using CoTaskMemAlloc. Crucially, it has a custom IUnknown::Release written in asm rather than vba. This Release pops a messagebox and calls cotaskmemfree on the instance, even if End or a stop button is hit. So guaranteed cleanup

@cristianbuse
Copy link
Owner

cristianbuse commented Oct 12, 2023

@Greedquest

git config --global core.autocrlf false

It was indeed set to true but I was under the impression that .gitattributes takes precedence over autocrlf. It also seems the data on GitHub has the wanted crlf - just the download raw button is the issue.

But I got my asm working!

Would definitely want to see that working because I tried following your SO posts and it's beyond my skill level.

I don't understand that trick - what is the magic number and what is the argument alignment issue it solves.

The magic number is an offset. The AddressOf operator returns the address of a delegate, not the actual function address.

Consider the following scenario:

  • a UserForm1 with code:
Option Explicit

Private Declare PtrSafe Function IUnknown_GetWindow Lib "shlwapi" Alias "#172" (ByVal pIUnk As IUnknown, ByVal hWnd As LongPtr) As Long

Private m_hWnd As LongPtr

Private Sub UserForm_Initialize()
    IUnknown_GetWindow Me, VarPtr(m_hWnd)
End Sub

Public Property Get GetHWnd() As LongPtr
    GetHWnd = m_hWnd
End Property
  • a standard module:
Option Explicit

Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr

Private f As UserForm1

Sub TestTimer()
    Set f = New UserForm1
    SetTimer f.GetHWnd, 1, 100, AddressOf TimerProc
End Sub

Sub TimerProc(ByVal hWnd As LongPtr, ByVal wMsg As Long, ByVal nIDEvent As LongPtr, ByVal wTime As Long)
    Stop 'BOOM. Crashes on x64, cannot step through code. Works fine on x32
    KillTimer hWnd, nIDEvent
End Sub

When running TestTimer, the first time the callback is called, code stops on the Stop statement which causes a crash on x64 (same issue with a mouse hook). Works fine on x32.

Now consider this variation of the standard module (no parameters in the callback):

Option Explicit

Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr

Private f As UserForm1
Private m_hwnd As LongPtr
Private m_nIDEvent As LongPtr

Sub TestTimer()
    Set f = New UserForm1
    m_hwnd = f.GetHWnd
    m_nIDEvent = 1
    SetTimer m_hwnd, m_nIDEvent, 100, AddressOf TimerProc
End Sub

Sub TimerProc()
    Stop 'Works fine on both x64 and x32
    KillTimer m_hwnd, m_nIDEvent
End Sub

which proves that something is wrong with how the parameters are pushed on the stack.

Finally, this fixes the issue:

Option Explicit

Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr

Private f As UserForm1

Sub TestTimer()
    Set f = New UserForm1

    Dim ptrFake As LongPtr: ptrFake = VBA.Int(AddressOf FakeProc) 'Points to a delegate object, not the function itself
    Dim ptrProc As LongPtr: ptrProc = VBA.Int(AddressOf TimerProc)
    '
    Dim addrFake As LongPtr: addrFake = ptrFake + PTR_SIZE * 6 + 4 'PTR_SIZE * 6 + 4 = 52 on x64
    Dim addrProc As LongPtr: addrProc = ptrProc + PTR_SIZE * 6 + 4
    '
    MemLongPtr(addrFake) = MemLongPtr(addrProc) 'Swap the function that the delegate actually calls from FakeProc to TimerProc
    '
    SetTimer f.GetHWnd, 1, 100, ptrFake 'Pass the address of the fake delegate which then points to the correct callback
End Sub


Sub FakeProc() 'Never called
End Sub

Sub TimerProc(ByVal hWnd As LongPtr, ByVal wMsg As Long, ByVal nIDEvent As LongPtr, ByVal wTime As Long)
    Stop 'Works fine on both x64 and x32
    KillTimer hWnd, nIDEvent
End Sub

I'm sure it will make sense now. This obviously doesn't solve the issue when 'Reset' is pressed in the IDE but that's only an issue within the scope of the actual callback, hence I am working around that by postponing the processing to a Terminate event which will be asyncronous like here. At least this trick allows debugging 😄 .

I would of course want to get rid of any crashes once and for all hence very curious of what you've achieved with asm. Many thanks!

@Greedquest
Copy link

Greedquest commented Oct 12, 2023

OK so at (AddressOf ...Proc) + 52, there exists some value x which you swap in FakeProc for the same value from TimerProc.

What exactly goes on in those first 52 bytes of "prologue"?

So you have the prologue from FakeProc, but the actual body from TimerProc. That prologue is the setup for a Sub() with zero args prologue rather than a Sub() with 4 args, which by trial and error you have found is what the debugger requires to not crash things. Strange. What's strange is that for 64 bit, the first 4 args are passed in registers not on the stack. So I'm more surprised you have weird stack misalignment in 64 bit than in 32 bit.

I guess all we can say is that those 52 bytes of prologue code in the version with args are corrupting the registers or the stack somehow.

Incidentally have you tried using a debugger. I have been experimenting with one called x64 debug and it's actually surprisingly easy. I solved all the asm questions pretty rapidly (was a bug in tB :'( I was stuck on for ages - my asm worked in VBA. Still tB is great for trial and error since it doesn't totally crash when you do something dumb)

PS thanks for the thorough answer!

@cristianbuse
Copy link
Owner

@Greedquest

What exactly goes on in those first 52 bytes of "prologue"?

They are identical. Seems like a signature thing for some kind of delegate object. I think a good analogy is that I am swapping a method address inside some weird virtual table.

which by trial and error you have found is what the debugger requires to not crash

Yes 😆 . Lots of trial and error

What's strange is that for 64 bit, the first 4 args are passed in registers not on the stack.

I read a while ago about this on x32 and x64 when doing the instance redirection in LibMemory so I am surprised as well. There are a lot of x64 bugs though - just one more.

Incidentally have you tried using a debugger.

No, I did not know there is such a thing for VBA. Can you please give me a link?

Also, where can I find the finalized ASM bit?

Many thanks!

@Greedquest
Copy link

Went away, hence no reply!

The debugger I've been using is called x64 dbg https://x64dbg.com/ . It's pretty cool, I tried winDBG or something like that which Microsoft makes and looks a lot like an office product, however if turned out to be command based rather than graphical like the vba debugger.
They're both assembly code debuggers - i.e. they give the raw bytes in memory and let you step through each instruction. What's great with x64 debug is it annotates all the assembly opcodes, uses arrows to show the jumps, even matches function pointers to known dll entries so you can see what function is being called.

The way I used it is launch excel or twinbasic. Open the debugger and attach to process excel.exe you need to learn the keyboard shortcut for "continue without breaking on exceptions". Run VBA and put a vba breakpoint somewhere in the code. Now use hex( adressof foo) or varptr(myassemblycode) to get a pointer to the stuff you are interested in in the immediate window.

While VBA is paused, Back in the assem debugger, hit ctrl g and paste the hex to jump to that address in memory. Now put a breakpoint here (click in margin just like in vbide) and continue execution of the VBA. Now the breakpoint gets hit - vbide freezes but the debugger lets you step through.

Inspect registers set values, jump to a line. Really good.

That's my improvised approach anyway, and it helped me find some tricky bugs in my injected assembly (tB passes by ref the this ptr, but VBA passes by val! Nasty bug)

I recommend this video it's only 8 mins and was the only thing I watched:

https://youtu.be/vq_VkoCgk3c?si=WheQhnh_c8D-4bSd

(I don't necessarily agree with what he's doing but the tutorial is pretty great if you have background knowledge)


RE the code I've been working on, I need to do one more thing which is to call IUnknown:: Release - you can imagine this is important for a cleanup routine.

But I've got it working to display a message box and call CoTaskMemFree after a stop button press or an end statement, which I'm pretty happy about. I also need to write the 32 bit version but that can wait.

I'll probably post on CR a write up because it is quite interesting but I'm really out of my depth and need bug checking

@cristianbuse
Copy link
Owner

Many thanks @Greedquest for the detailed guidance.

@cristianbuse
Copy link
Owner

Hi @Greedquest ,

This might be of interest to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants