안녕하세요.
아래는 인터넷에 돌아다니는 시계 소스입니다.
A1셀에 시간이 12:32:00 의 형식으로 보여지며 매 1초마다 시간이 갱신되요.
근데 이게 64비트에서는 컴파일도 되고, 에러도 없습니다. 시계 [ 시작 ] 누르면 시간도 잘 갑니다. 근데 32bit 에서는 컴파일에러에 변수선언도 잘못되었다고 나오고 소스를 고치면 컴파일은 또 잘 되는데, 정작 시계 [ 시작 ] 누르면 에러창이 뜹니다.
그 이유를 인터넷에 찾아서 뒤져봤는데 64bit 는 Long 변수타입이 달라 LongPtr을 해야한다든가, Function 앞에 PtrSafe를 부쳐야 한다든가, 뭐 대충 변수범위가 64bit 와 32bit 가 서로 달라서 생기는 문제인듯 해요.
근데 그렇게 이해만 했지, 문제를 해결하지는 못했어요. 제가 원하는 것은 64비트에서 잘 되는 것이 아니라, 32비트에서도 잘 되는 것을 원합니다. 현재 올려드린 소스는 64비트에서 매우 잘 되는 소스입니다.
# If Win32 Then
#Else Win64 Then
이 문구도 써봤는데 실패했습니다.
제가 원하는 것 :
위 시계가 64bit 와 32bit 모두 양쪽에서 아무 컴파일에러도 없이 잘 돌아가게 소스를 고쳐주시면 감사드리겠습니다. LongPtr 아무리 삽입/삭제 해도 안되더라구요. 컴파일 후에 시계 [ 시작 ] 을 누르면 정말 시계가 잘 갱신되면 좋겠습니다. 소스를 고치면, 컴파일에러는 사라지더라도 정작 [ 시작] 을 누르면 또 에러가 나기도 합니다.
감사합니다.
제가 시도했던 것 :
32bit 에서는 function 선언 앞에 불필요한 ptrsafe f를 삭제하라고 해서 했더니, 컴파일 됨. 근데 시계는 안감.
32bit 에서 사용안하는 일단 LongLong 을 Long으로 변경했지만, 컴파일 에러는 멈추지 않음.
32bit 엑셀이 없어서 테스트가 안되는군요..
선생님 안녕하세요. 네. 저도 그동안 64비트만 써서 32bit에서 에러가 있다는 사실을 몰랐습니다. 요즘은 다들 64비트를 사용해서인지 32비트 엑셀 찾기도 힘들더라구요. 어쨌든 관심에 감사드립니다.
안녕하세요~
지금 올리신 타이머 코드는
64비트 Excel/VBA에서는
PtrSafe, LongPtr, LongLong 선언이 운영체제의 64비트 WinAPI와 잘 맞아서
컴파일도 실행도 잘 맞습니다.
하지만
32비트 Excel/VBA에서는
특히 구버전 VBA6, Office 2007이하에서
PtrSafe, LongPtr, LongLong 자체를 인식하지 못하고,
또 어떤 경우에는 인식은 하고 있지만,
WinAPI 선언부의 LongLong이 Win32 API 시그니처와 안 맞아서
결과적으로
처음에는 타입을 모르니 선언 문법오류로
컴파일 오류가 나고
또한 타입을 바꾸다보면 컴파일은 통과하지만
이번에는 실행 중 API 호출이 깨져서
런타임 에러가 나는 상황일 수 있습니다.
따라서
코드를 VBA버전에 따라 VBA7 여부와 비트수(32/64bit)에 맞게
조건부 컴파일 해서
VBA7(Office 2010 이상)은 PtrSafe, LongPtr 사용하도록 하고,
VBA6(Office 2007 이하)는 PtrSafe, LongPtr 없이 Long 사용하도록 합니다.
WinAPI 시그니처는
64bit의 포인터 자리는 LongPtr
32bit의 포인터 자리는 Long
LongLong은 “포인터 표현용 타입”으로는 64bit 전용에 가깝고,
숫자 연산용으로는 32bit VBA7에서도 쓸 수 있지만
Win32 API 포인터 자리에서는 절대 쓰면 안됩니다.
1. "같은 코드인데 64비트 Excel에서는 문제 없이 잘 돌아가는데,
32비트 Excel에서는 컴파일 에러가 나거나, 변수 선언이 잘못됐다고 나옵니다.
이유가 뭔가요?”
이는
VBA버전과 비트수가 다르기 때문입니다.
올려주신 선언부는 VBA7 + 64bit 기준으로 맞춰져 있기 때문입니다.
PtrSafe, LongPtr, LongLong 은 전부 구버전(32bit VBA6)에서는 모르는 타입이라서
바로 컴파일 에러가 터집니다.
Office 2010 이후 32bit 환경이라도
LongLong은 64bit 전용 개념이라 32bit WinAPI 시그니처와 안 맞아서 문제가 됩니다.
그래서 32bit 쪽에서 맞추려면
PtrSafe를 쓸 수도 있고 (VBA7), 아닐 수도 있고 (VBA6)
포인터 타입을 Long 또는 LongPtr로 상황에 맞게 바꿔야 하는데,
이것을 조건부 컴파일(#If VBA7 Then …) 로 나누지 않으면
한쪽은 항상 에러가 납니다.
2. “32bit에서 컴파일 에러를 없애보려고 타입을 바꾸다 보면,
컴파일은 잘 되는데 [시작] 버튼을 누르면 런타임 에러가 뜹니다.
왜 이런가요?”
이는,
WinAPI 선언과 실제 함수 시그니처가 다르면
컴파일은 통과했을지라도
실제 호출 시 스택이나 포인터가 꼬여서 실행 중에 크래시나 런타임 에러가 발생합니다.
예를 들어
좀 단순화했지만, SetTimer는 실제 Win32 API에서 이런 형태입니다.
32bit에서는 UINT_PTR, HWND, 함수 포인터 등이 4바이트(Long)이고,
64bit에서는 이 값들이 8바이트(LongLong)가 됩니다.
VBA에서는 이를 LongPtr로 추상화합니다.
그런데 32bit 환경에서
라고 해버리면
콜백 함수 주소를 8바이트 타입처럼 다루게 되고
실제 Win32 API는 4바이트 포인터를 기대하는데
바로 스택이 틀어지면서 런타임 에러가 납니다.
즉,
“컴파일이 된다”라는 것이 꼭 “타입이 맞다”는 아닙니다.
운영체제의 실제 함수 정의에 맞춰서
32bit에서는 포인터 타입을 Long, 64bit에서는 LongPtr로 맞춰줘야
비로소 런타임까지 안전하게 됩니다.
3. “위의 시계를 64bit와 32bit 둘 다에서 컴파일/실행이 모두 되게 하려면,
SetTimer / KillTimer, 그리고 콜백 함수 선언을 어떻게 써야 하나요?
#If Win32 / #Else Win64를 써봤지만 실패했습니다.”
이에 대한
핵심 포인트는 세 가지입니다.
첫번째 기준은 Win32/Win64가 아니라
VBA7 여부부터 결정해야 합니다.
MS에서 공식으로 제시하는 패턴은
https://learn.microsoft.com/en-us/office/client-developer/shared/compatibility-between-the-32-bit-and-64-bit-versions-of-office
입니다.
VBA7인 경우에만 PtrSafe, LongPtr 키워드를 지원하기 때문에
구버전(VBA6)에서는 이 키워드 자체가 문법적 에러가 발생합니다.
이 코딩을
If VBA7 Then ' // Office 2010 이상 (32/64bit 둘 다) #Else ' // Office 2007 이하 #End If
이렇게 코딩해야 합니다.
그리고 필요하다면 #If Win64 Then을 안쪽에서 추가로 쓰는 식입니다.
두 번째, 포인터 타입은 VBA7에서는 LongPtr, 구버전에서는 Long
64bit에서 Long은 여전히 4바이트라서 포인터 크기와 안 맞고,
이걸 해결하려고 나온 게 LongPtr입니다.
하지만 VBA6에는 LongPtr이 없으니,
VBA7은 LongPtr
VBA6은 Long
이렇게 조건부로 나눠야 합니다.
마찬가지로 KillTimer, TimerID 변수도 같은 패턴으로 맞춰야 합니다.
그리고 세 번째, LongLong은 아예 쓰지 않는 편이 안전합니다.
LongLong은 64bit에서 유용하지만,
WinAPI 선언에서는 이미 LongPtr이 포인터 사이즈를 추상화해 주고,
VBA6에서는 LongLong을 아예 지원하지 않고,
VBA7 32bit에서는 LongLong은 “일반 숫자 계산용”으로는 사용할 수 있지만,
포인터 자리(WinAPI 호출)에는 쓰면 안됩니다.
Win32 API 시그니처와 맞지 않아 런타임 문제를 만들 가능성이 크니,
따라서 이 케이스에서는,
“콜백 주소(AddressOf … 받는 자리)”도 LongPtr(VBA7) / Long(VBA6) 조합으로 맞추고
LongLong은 사용하지 않는 것이 좋습니다.
4. “위 시계가 64bit 와 32bit 모두 양쪽에서 아무 컴파일에러도 없이 잘 돌아가게 소스를 고쳐주시면 ...”
Option Explicit '// ========================= '// 공통 변수 '// ========================= Private Count As Long #If VBA7 Then '// Office 2010 이상 (32/64bit 모두 포함) : PtrSafe / LongPtr 사용 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 Declare PtrSafe Function KillTimer Lib "user32" ( _ ByVal hWnd As LongPtr, _ ByVal nIDEvent As LongPtr) As Long Public TimerID As LongPtr '// 타이머 ID(포인터 크기와 맞춤) #Else '// Office 2007 이하 (무조건 32bit) : PtrSafe / LongPtr 없음 Private Declare Function SetTimer Lib "user32" ( _ ByVal hWnd As Long, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As Long) As Long Private Declare Function KillTimer Lib "user32" ( _ ByVal hWnd As Long, _ ByVal nIDEvent As Long) As Long Public TimerID As Long '// 32bit에서는 그냥 Long 사용 #End If '// ========================= '// 외부에서 호출하는 엔트리 '// 버튼 [시작] / [정지]에 연결 '// ========================= Public Sub StartCount() '// 최초 카운트 초기화 Count = 0 '// 1초마다 타이머 (원하면 3으로 바꿔서 3초마다로 가능) StartTimer 1 End Sub Public Sub StopCount() DeactivateTimer TimerID End Sub Public Sub StartTimer(ByVal Seconds As Long) '// Seconds 간격(초)으로 Timer 시작 ActivateTimer Seconds, AddressOf TriggerEvent, TimerID End Sub '// ========================= '// SetTimer가 호출하는 콜백 '// 파라미터 타입이 32/64bit에 맞아야 함 '// ========================= #If VBA7 Then Private Sub TriggerEvent( _ ByVal hWnd As LongPtr, _ ByVal uMsg As Long, _ ByVal idEvent As LongPtr, _ ByVal SysTime As Long) '// DWORD는 4바이트 #Else Private Sub TriggerEvent( _ ByVal hWnd As Long, _ ByVal uMsg As Long, _ ByVal idEvent As Long, _ ByVal SysTime As Long) #End If On Error Resume Next EventFunction End Sub '// ========================= '// 타이머 시작/정지 내부 함수 '// ========================= #If VBA7 Then Private Function ActivateTimer( _ ByVal Seconds As Long, _ ByVal FunctionAddress As LongPtr, _ ByRef tID As LongPtr) #Else Private Function ActivateTimer( _ ByVal Seconds As Long, _ ByVal FunctionAddress As Long, _ ByRef tID As Long) #End If On Error Resume Next '// 이미 돌고 있지 않을 때만 새로 시작 If tID = 0 Then tID = SetTimer(0, 0, Seconds * 1000, FunctionAddress) End If End Function #If VBA7 Then Private Function DeactivateTimer(ByRef tID As LongPtr) #Else Private Function DeactivateTimer(ByRef tID As Long) #End If On Error Resume Next If tID <> 0 Then If KillTimer(0, tID) <> 0 Then tID = 0 End If End If End Function '// ========================= '// 매 틱마다 실제로 하고 싶은 작업 '// ========================= Private Sub EventFunction() On Error Resume Next Dim tm As Date tm = Time With ThisWorkbook.Worksheets("Sheet1") '// 시트명 맞게 수정 .Cells(1, 1).Value = Format$(tm, "hh:nn:ss") '// A1에 12:32:00 형식 표기 End With Count = Count + 1 'Debug.Print "Count:" & Count & " now:" & tm End Sub안녕하세요.
아~ 일단 인사부터 먼저 올리겠습니다. 감사감사합니다. 수메리안님. 도대체 뭐하시는 분이신지요? 혹시 MS 엑셀개발팀에서 일하시는 분이신가요? 여기 이 커뮤니티에 고수 선생님들 꽤 계십니다. (더블유에이님, 원조백수님, 마법의손님, 다른 분들도요)
근데 수메리안님은 용어 하나하나까지 경이롭기까지 할 정도로 정확히 답글을 달아주시니 제가 몸둘 바를 모르겠습니다. 이런 걸 보면 정말 선생님같기도 하구요. 뭔가 통달하신 분 같아요. 단순 코딩고수님을 넘어서서 엑셀의 근본원리를 꿰뚫고 계시는 분 같아 경이로울 정도입니다. 이런 귀하신 분으로부터 직접 가르침을 받을 수 있으니 이 커뮤니티가 저에게는 축복중의 축복입니다. 제가 2주일 머리싸매고 스트레스받던 문제를 하루도 안되 해결해 버리시네요.
너무 감사드립니다. 넙죽 인사올립니다.
1. 제가 이 시계가 필요했던 이유 :
사용자정의 Function을 만들어 사용중인데 리턴값을 주기적으로 갱신시킬려고 하니 변동이벤트가 필요했고 셀에 시계를 삽입하여 매 1초당 셀값이 갱신되면 제가 원했던 사용자정의 함수도 덩달아 갱신되는 장치가 필요했었습니다.
그래서 미국 커뮤니티 Reddit 엑셀포럼에 물어보게 되었고, 이 시계 소스를 발견하게 되었습니다. 아주 만족스럽게 사용하던차, 32bit 에서는 에러가 발생한다는 사실을 알게 되었습니다. 제가 그동안 64bit 에서만 사용하고 있어서 이 사실을 모르고 있었거든요.
2.문제해결을 위하여 제가 시도했던 것들 :
64bit 를 32bit 에 맞게 수정하는 건 아주 단순할 것이라고 예상했었습니다. 인터넷으로 공부해보니, 에러 발생원인은 Long Type 으로 선언된 변수에 있었다는 것을 알게 되었고, 일단 최초에는 응급처치로, 소스에 있던 64bit 용 LongPtr을 다 삭제하고 32bit 용 변수인 Long 으로만 고치고. 64bit 에서 사용안하는 LongLong 도 Long 으로 고치고요. 저는 1초 단위의 시간만 필요할 뿐, LongLong 타입의 극미세한 시간까지는 잴 필요가 없었거든요. 어쨌든 이런 조치를 취하면 이제 아무런 문제가 없을 줄 알았습니다. 그런데 왠걸, 컴파일에러, 런타임에러, 변수선언 에러, 에러는 에러의 꼬리를 물고 한없이 무한루프로 발생하게 되더라구요. 예를 들어 이것을 고치면 컴파일은 되는데, 런타임 에러가 나고 등등. 하나를 고쳐도 다른 뭔가가 잘 안맞더군요. 그래서 공부를 더 해보니 Pointer 라는 용어까지 알아야 하더라구요. 입력값과 리턴값의 메모리의 사이즈가 상호호응해야한다는 말같은데 32bit와 64bit 가 이 포인터가 달라서 리턴값이 다르게되니 에러도 나고, 예상치못한 계산오류도 생긴다는 것을 알게 되었습니다. 변수선언 Date Type 에러에서 기인한다는 것은 알게되었지만, 이를 해결하지 못한 상태에서, 또한 이걸 32bit와 64bit 양쪽 모두에서 사용가능하게 할려고 소스를 고치다보니 너무 힘들어 여기에 질문하게 되었습니다.
3. 소스에 한글주석까지 친절하게 달아주셔서 너무 감사드립니다.
코딩을 정확하게 이해하시고 답글을 달아주시니 제가 어떻게 보답해야 할 지 모르겠습니다. 너무너무 감사드립니다. 제가 올려드린 소스에서 아주 많은 부분을 고쳐주셨네요. 논리적으로 아주 단순화, 일목요연화시켜 주신 것 같습니다. Office 2010 이상부분과, 그 이전 버전처리를 어떻게 해야할 지 소스상에서 보여주셔서 공부가 많이 될 것 같습니다. 소스 내에서 SetTimer 와 KillTimer가 무슨 역할을 하는 지 어렴풋하게 알고 있었던 것을 아주 명확하게 주석처리해주셔서 아~ 여기에서 이렇게 포인터가 꼬이게 되면 에러가 날 수도 있겠구나~ 하고 명확화해주신 것 같습니다. TriggerEvent 같이 파라미터들의 Data Type 이 매우 복잡하게 보이는데 이것도 32bit와 64bit 양쪽 가능하게 명확하게 선언해주셨는지(사실 아직도 전 이해를 못하고 있습니다.) 입을 다물지 못하겠네요.
4. 수정된 코드로 실제로 32bit 에서 에러없이 잘 돌아가는 짤까지 올려주셔서 감동입니다.
32bit 엑셀은 요즘 같은 고사양컴이 흔한 시대는 잘 사용하지 않아 그런 컴을 발견하기도 힘들어 테스트도 힘들었을텐데, 짤에 보면 진짜 32bit 컴을 확인해주시네요. 또한 테스트시간이 오전 10시경 인데.. 시계가 테스트라서 시간이 찍힐 수 밖에 없잖아요. 그렇다면 아침에 소스를 고쳐주셨다는 짐작을 할 수 있는데, 뭐하시는 분이신가요? 회사재직중이라면 일해야할 시간이니 그런 분 같지도 않고, 아침에 일어나 계시니 밤샘하시는 엑셀오타쿠도 아니신 듯 하고, 궁금합니다. 올려주신 소스 32bit 64bit 양쪽에서 아주 잘 돌아가는 것을 확인하였습니다. 거듭 감사드립니다. 소스에 주석 달아주셔서 또한 더욱 감사드리고 공부에 참고하도록 하겠습니다.
VB6.0 64bit 스타일로 VBA 코딩해 개발한 것으로 보입니다. 내장 객체가 없으면 API를 쓰는거지. 저런식으로 누가 쓸까요? 생성형ai한테 부탁하세요.
네. 맞습니다. LongPtr 을 사용하는 걸 보면 64비트용이라는 것을 바로 알 수 있죠. 제가 이 문제를 해결할려고 AI 한테도 물어봤는데요. AI는 인터넷에 올려진 글들의 나열일뿐, 인터넷에 안돌아다니는 말까지 스스로 학습하여 에러를 잡아낼 정도로 섬세하진 않더라구요. 앞으로 10년 후에는 달라질 수 있을려나 모르겠습니다. 어쨌든 관심에 감사드립니다.