엑셀 VBA 32비트·64비트 Declare 문 완벽 호환 가이드(PtrSafe, LongPtr, 조건부 컴파일)

이 글의 목적은 엑셀 VBA에서 Windows API를 호출할 때 32비트와 64비트 환경 간 Declare 문 호환 문제를 근본적으로 해결할 수 있도록, PtrSafe·LongPtr·LongLong·조건부 컴파일(#If VBA7, #If Win64) 사용 원칙과 안정 패턴, 점검 체크리스트, 실무 예제 코드를 체계적으로 정리하는 것이다.

1. 왜 32비트·64비트 호환이 깨지는가

VBA 코드에서 Windows API를 Declare로 호출할 때, 포인터 크기 차이로 인해 인수형과 반환형이 달라져 호출 규약이 깨지는 경우가 발생한다. 32비트에서는 포인터가 4바이트이고 64비트에서는 8바이트이다. VBA7부터는 포인터 크기에 적응하는 LongPtr 형과 64비트 정수형 LongLong 그리고 64비트에서 필수인 PtrSafe 한정자를 제공한다. Office 2010 이상은 VBA7이며 32비트/64비트 에디션 모두 존재한다. Office 2007 이하(VBA6)는 32비트만 존재하며 PtrSafeLongPtr을 인식하지 못한다.

주의 : 64비트 Office에서 Declare에는 반드시 PtrSafe 한정자를 붙여야 한다. 누락 시 컴파일 오류가 발생한다.

2. 핵심 키워드 정리

키워드설명비트별 의미비고
PtrSafeDeclare가 64비트에서도 안전함을 선언한다.64비트 필요, 32비트(VBA7) 사용 가능VBA6에서는 문법 오류이다.
LongPtr포인터 크기에 맞춰 32/64비트에서 자동 크기 조정된다.32비트=4바이트, 64비트=8바이트핸들(HANDLE), 포인터, 포인터 크기 관련 인수에 사용한다.
LongLong고정 64비트 정수형이다.둘 다 8바이트카운터·타임스탬프 등 큰 정수 처리용이다.
#If VBA7VBA7(Office 2010+) 여부 분기이다.VBA6에서는 FalsePtrSafe·LongPtr 사용 코드 보호에 필요하다.
#If Win64프로세스가 64비트인지 분기한다.64비트 TrueAPI 엔트리명 등 분기 시 사용한다.

3. 올바른 형 선택 규칙

  • 포인터·핸들·윈도우 핸들(HWND, HMODULE, HDC 등)·메모리 주소·콜백 포인터에는 LongPtr을 사용한다.
  • API가 64비트 정수를 명시적으로 요구할 때만 LongLong을 사용한다.
  • 문자 개수·인덱스·상태코드 등 고정 32비트 값에는 Long을 유지한다.
  • 문자열 버퍼는 String 또는 ByVal As String, 바이너리 버퍼는 ByVal As LongPtr + ByVal As Long 길이 또는 Byte() 배열을 사용한다.
주의 : 64비트에서도 Long은 여전히 4바이트이다. 포인터를 Long에 저장하면 잘린다.

4. 조건부 컴파일 기본 패턴

아래 패턴은 VBA6~VBA7, 32~64비트 전 범위를 안전하게 커버한다. VBA6에서 문법 오류가 나지 않도록 PtrSafeLongPtr이 등장하는 모든 구문은 #If VBA7 Then 블록 안에 넣는다.

' 모듈 상단 #If VBA7 Then ' 32/64비트 Office 2010+ 공용 Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) #Else ' Office 2007 이하 Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) #End If
Sub DemoSleep()
Sleep 100
End Sub

핸들이나 포인터를 주고받는 API는 LongPtr을 사용한다.

#If VBA7 Then Private Declare PtrSafe Function FindWindowA Lib "user32" ( _ ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr #Else Private Declare Function FindWindowA Lib "user32" ( _ ByVal lpClassName As String, ByVal lpWindowName As String) As Long #End If 

5. GetWindowLongPtr/SetWindowLongPtr 호환 선언

64비트에서는 GetWindowLong 계열이 GetWindowLongPtr로 승격된다. 32비트에서는 기존 API명을 사용해야 한다. 엔트리명은 운영체제 DLL에 고정되어 있으므로 별도 분기가 필요하다.

#If VBA7 Then #If Win64 Then Private Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongPtrA" ( _ ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr Private Declare PtrSafe Function SetWindowLongPtr Lib "user32" Alias "SetWindowLongPtrA" ( _ ByVal hWnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr #Else Private Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongA" ( _ ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr Private Declare PtrSafe Function SetWindowLongPtr Lib "user32" Alias "SetWindowLongA" ( _ ByVal hWnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr #End If #Else Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" ( _ ByVal hWnd As Long, ByVal nIndex As Long) As Long Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" ( _ ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long #End If 
주의 : 32비트에서 GetWindowLongPtr라는 엔트리는 존재하지 않는다. 64비트에서는 반대로 GetWindowLong로 핸들을 받으면 포인터가 잘린다.

6. RtlMoveMemory(CopyMemory) 안전 선언

메모리 복사 API는 포인터 인수와 길이 인수를 정확히 선언해야 한다. 문자열·배열·UDT 복사 등에서 자주 쓰인다.

#If VBA7 Then Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr) #Else Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ ByVal Destination As Long, ByVal Source As Long, ByVal Length As Long) #End If 

주소 취득은 StrPtr·VarPtr·ObjPtr를 사용한다. 반환값은 포인터이므로 LongPtr 변수에 저장한다.

Sub DemoCopy() Dim s As String s = "ABC" #If VBA7 Then Dim p As LongPtr #Else Dim p As Long #End If p = StrPtr(s) ' p가 가리키는 메모리를 목적지로 복사하는 등 후속 처리 End Sub 

7. 콜백(AddressOf)와 함수 포인터

윈도우 훅, 타이머 콜백, Enum 계열 API는 콜백 포인터를 전달한다. 포인터 크기가 다르므로 LongPtr로 선언한다.

#If VBA7 Then Private Declare PtrSafe Function EnumWindows Lib "user32" ( _ ByVal lpEnumFunc As LongPtr, ByVal lParam As LongPtr) As Long #Else Private Declare Function EnumWindows Lib "user32" ( _ ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long #End If
Function EnumProc(ByVal hWnd As LongPtr, ByVal lParam As LongPtr) As Long
EnumProc = 1
End Function

Sub RunEnum()
#If VBA7 Then
EnumWindows VBA.CLngPtr(AddressOf EnumProc), 0
#Else
EnumWindows AddressOf EnumProc, 0
#End If
End Sub
주의 : 일부 콜백은 StdCall 규약을 암묵적으로 가정한다. VBA의 사용자 정의 함수는 기본적으로 호환되나, 인수 개수와 형이 정확히 일치해야 한다.

8. UDT(사용자 정의 형식)와 LongPtr

VBA7에서는 UDT 내부에도 LongPtr를 사용할 수 있다. 레이아웃이 포인터 크기에 맞춰 자동 조정되므로 패딩까지 포함해 안정적이다.

Option Explicit
#If VBA7 Then
Public Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type

Public Type WINDOWINFO
    cbSize As Long
    rcWindow As RECT
    rcClient As RECT
    dwStyle As Long
    dwExStyle As Long
    dwWindowStatus As Long
    cxWindowBorders As Long
    cyWindowBorders As Long
    atomWindowType As Integer
    wCreatorVersion As Integer
    hOwner As LongPtr  ' 포인터/핸들은 LongPtr
End Type
#Else
Public Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Public Type WINDOWINFO
cbSize As Long
rcWindow As RECT
rcClient As RECT
dwStyle As Long
dwExStyle As Long
dwWindowStatus As Long
cxWindowBorders As Long
cyWindowBorders As Long
atomWindowType As Integer
wCreatorVersion As Integer
hOwner As Long ' 32비트 핸들
End Type
#End If

9. 흔한 오류와 해결

증상원인해결
Compile error: The code in this project must be updated for use on 64-bit systems64비트에서 PtrSafe 미지정모든 Declare에 PtrSafe 추가한다.
Bad DLL calling convention인수형/반환형 불일치, 포인터를 Long로 선언핸들·포인터를 LongPtr로 변경한다.
Entry Point Not Found32/64비트에서 잘못된 엔트리명 사용#If Win64 분기로 LongPtr 버전 API 엔트리를 매핑한다.
Type mismatch 또는 Access violationUDT 레이아웃 불일치, 잘린 포인터UDT 내 포인터는 LongPtr, 구조체 크기 필드(cbSize) 재검증한다.
Argument not optionalDeclare 시그니처 인수 개수 불일치공식 시그니처와 동일하게 선언한다.

10. 마이그레이션 체크리스트

  1. 대상 모듈의 모든 Declare를 검색한다.
  2. 64비트에서도 사용할 코드에는 #If VBA7 Then 블록을 도입한다.
  3. 포인터·핸들·콜백 형을 LongPtr로 교정한다.
  4. 반환형이 포인터인 API의 변수 선언도 LongPtr로 교정한다.
  5. 엔트리명이 ...LongPtr로 승격된 API(Get/SetWindowLongPtr 등)를 #If Win64로 분기한다.
  6. UDT 내부 포인터 필드를 LongPtr로 재정의한다.
  7. 테스트: 32비트·64비트 Office 각각에서 컴파일과 런타임 동작을 확인한다.

11. 실무 템플릿: 공용 선언 모듈

아래 모듈 하나로 32비트/64비트와 VBA6/7을 폭넓게 커버한다. 새 프로젝트에 그대로 이식하여 사용한다.

Option Explicit '===== Common API Declarations for x86/x64, VBA6/VBA7 =====
' Compile-time feature switches
#Const HAS_VBA7 = VBA7
#If VBA7 Then
#Const HAS_WIN64 = Win64
#End If

' --- Kernel32 ---
#If VBA7 Then
Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Public Declare PtrSafe Function GetTickCount64 Lib "kernel32" () As LongLong
#Else
Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
' GetTickCount64 미지원, 32비트 대체
Public Declare Function GetTickCount Lib "kernel32" () As Long
#End If

' --- User32: Window Long ---
#If VBA7 Then
#If Win64 Then
Public Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongPtrA" ( _
ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
#Else
Public Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongA" ( _
ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
#End If
#Else
Public Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" ( _
ByVal hWnd As Long, ByVal nIndex As Long) As Long
#End If

' --- Memory move ---
#If VBA7 Then
Public Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr)
#Else
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
ByVal Destination As Long, ByVal Source As Long, ByVal Length As Long)
#End If

' --- Helper: pointer size at runtime ---
Public Function PointerSizeBytes() As Long
#If VBA7 Then
' String 포인터 크기 측정
PointerSizeBytes = LenB(StrConv("A", vbFromUnicode))
' 위 방식은 2를 반환한다. 정확한 포인터 크기는 조건부 상수로 판단한다.
#If Win64 Then
PointerSizeBytes = 8
#Else
PointerSizeBytes = 4
#End If
#Else
PointerSizeBytes = 4
#End If
End Function
주의 : 런타임에 포인터 크기를 직접 측정하기보다 #If Win64 조건부 상수를 신뢰하는 것이 안전하다. VBA의 문자열 내부 표현은 포인터 크기 탐지에 적합하지 않다.

12. API 문자열 버퍼 안전 패턴

ANSI 함수(엔트리명 접미사 A)에는 String을 직접 주고, 유니코드 함수(W)에는 StrPtrByte() 버퍼를 사용한다. 아래는 윈도우 제목을 받아오는 예시이다.

#If VBA7 Then Private Declare PtrSafe Function GetWindowTextA Lib "user32" ( _ ByVal hWnd As LongPtr, ByVal lpString As String, ByVal cch As Long) As Long #Else Private Declare Function GetWindowTextA Lib "user32" ( _ ByVal hWnd As Long, ByVal lpString As String, ByVal cch As Long) As Long #End If
Function GetTitle(h As LongPtr) As String
Dim buf As String
buf = String$(260, vbNullChar)
GetWindowTextA h, buf, Len(buf)
GetTitle = Left$(buf, InStr(buf, vbNullChar) - 1)
End Function

13. 대용량 카운터와 LongLong

QueryPerformanceCounterGetTickCount64 등은 64비트 정수를 반환한다. 해당 값은 LongLong으로 받는다.

#If VBA7 Then Private Declare PtrSafe Function GetTickCount64 Lib "kernel32" () As LongLong #End If
Sub DemoTick()
#If VBA7 Then
Dim t As LongLong
t = GetTickCount64()
Debug.Print t
#Else
Debug.Print "32비트에서는 GetTickCount 사용"
#End If
End Sub

14. 기존 32비트 선언을 자동 변환하는 규칙

  1. Declare 키워드 뒤에 PtrSafe를 추가한다.
  2. 반환형과 인수형 중 포인터/핸들은 LongLongPtr로 바꾼다.
  3. 엔트리명이 ...LongPtr로 바뀐 API는 #If Win64로 분기한다.
  4. UDT 내부 포인터 필드는 LongPtr로 바꾼다.
  5. 콜백 인수는 LongPtr로, AddressOf 결과는 CLngPtr로 변환해 전달한다.

15. 점검 자동화 매크로(정규식 기반)

아래 코드는 모듈 텍스트에서 위험 선언을 탐지하고 후보 교정안을 인쇄한다. 프로젝트에 맞게 수정하여 사용한다.

Sub AuditDeclares() Dim cm As CodeModule, i As Long, line As String For Each cm In ThisWorkbook.VBProject.VBComponents Set cm = cm.CodeModule For i = 1 To cm.CountOfLines line = cm.Lines(i, 1) If InStr(1, line, "Declare", vbTextCompare) > 0 _ And InStr(1, line, "PtrSafe", vbTextCompare) = 0 Then Debug.Print "PtrSafe 누락: "; cm.Name; ":"; i; " - "; Trim$(line) End If If InStr(1, line, " As Long)", vbTextCompare) > 0 And _ (InStr(1, line, "FindWindow", vbTextCompare) > 0 Or _ InStr(1, line, "Handle", vbTextCompare) > 0 Or _ InStr(1, line, "Wnd", vbTextCompare) > 0) Then Debug.Print "핸들 Long 사용 의심: "; cm.Name; ":"; i End If Next i Next cm End Sub 

16. 배포 전략

  • 회사 표준 모듈로 본문 11장의 공용 선언 모듈을 패키징한다.
  • 애드인(xlam) 또는 템플릿(xltm)로 배포하여 신규 파일이 동일 선언을 공유하게 한다.
  • CI 단계에서 64비트 Office로도 자동 컴파일 테스트를 수행한다.
  • 사용자 PC가 64비트 Office로 전환될 때, 기존 문서의 Declare 모듈만 교체하면 된다.

17. 빠른 참조표

상황형 선택비고
HWND/HANDLE/HMODULELongPtr반환형과 변수 모두 동일하게 한다.
콜백 포인터(AddressOf)LongPtrCLngPtr(AddressOf Proc)로 전달한다.
크기/길이 매개변수Long 또는 LongPtrAPI 정의에 따른다. 포인터 크기 기반이면 LongPtr이다.
64비트 카운터LongLong오버플로 방지이다.
문자열 버퍼String 또는 Byte()A/W 엔트리와 일치하게 선언한다.

FAQ

Office 2010 32비트에서도 PtrSafe가 필요한가

필수는 아니나 사용해도 된다. 단, VBA6(Office 2007 이하)에서 컴파일 오류가 나므로 #If VBA7 분기로 보호해야 한다.

LongPtr 대신 LongLong을 써도 되나

안 된다. LongLong은 고정 64비트 정수이다. 포인터는 32/64비트에 따라 크기가 달라지므로 LongPtr을 사용해야 한다.

Declare에 PtrSafe만 추가하면 끝나는가

아니다. 포인터·핸들을 LongPtr로 교정해야 한다. 또한 일부 API는 64비트에서 엔트리명이 LongPtr 버전으로 바뀐다.

UDT 내부에 LongPtr 사용이 가능한가

VBA7에서는 가능하다. VBA6 호환이 필요하면 #If VBA7 블록으로 구분한다.

런타임 오류 453(Entry Point not found)의 원인은 무엇인가

잘못된 엔트리명 또는 DLL 매핑 때문이다. 32비트에서는 GetWindowLongA, 64비트에서는 GetWindowLongPtrA를 사용해야 한다.