64-битный Office и старый код: Declare PtrSafe, LongPtr и совместимость с VB6

РубрикаСопровождениеУровеньСредний Чтение9 минОпубликовано ТегиVB6VBAOffice x64Windows API

С 2019 года Microsoft ставит 64-битный Office по умолчанию, и макросы с вызовами Windows API начинают падать ещё на этапе компиляции. Разбираем, как править объявления, что делать с общим кодом VB6 и VBA и какие поломки не лечатся одной правкой.

Главное. PtrSafe и LongPtr решают только часть проблемы. Каждое объявление API нужно сверить с документацией, структуры пересчитать, а внутрипроцессные 32-битные компоненты VB6 и OCX заменить, потому что в 64-битный Office они не загрузятся.

Почему проблема вылезла именно сейчас

Долгие годы корпоративный стандарт был простым: 32-битный Office на любой Windows. Начиная с Office 2019 и Microsoft 365 установщик по умолчанию ставит 64-битную редакцию, и при плановой замене ПК или переустановке пакета макросы, которые работали десятилетиями, перестают открываться.

Сам VBA при этом почти не изменился. Обычный код с циклами, ячейками и коллекциями переносится без правок. Ломаются три вещи: объявления функций Windows API через Declare, всё, что связано с указателями и дескрипторами, и внутрипроцессные компоненты, собранные под 32 бита, включая ActiveX DLL и OCX, написанные на VB6.

Declare без PtrSafe и ошибка компиляции

В 64-битном Office работает VBA 7 (он же есть и в 32-битном Office начиная с 2010). В нём любое объявление Declare обязано содержать ключевое слово PtrSafe. Без него проект не компилируется с сообщением о том, что код нужно обновить для 64-битных систем.

Важно понимать, что PtrSafe ничего не исправляет автоматически. Это обещание разработчика: «я проверил типы параметров». Если просто дописать PtrSafe и оставить Long там, где API ждёт указатель или дескриптор, компиляция пройдёт, а на вызове вы получите неверные данные или падение Excel. Поэтому каждое объявление нужно сверить с документацией функции.

Правило перевода простое: дескрипторы окон и объектов (HWND, HANDLE, HDC, HKEY), указатели (LPVOID, LPSTR, если передаются как адрес) и поля размера, которые в C объявлены как SIZE_T, ULONG_PTR или LONG_PTR, становятся LongPtr. Обычные числа DWORD, UINT, BOOL и INT остаются Long, потому что в Windows x64 они по-прежнему 32-битные.

Условная компиляция для общего кода

Модули часто живут в двух мирах сразу: часть пользователей ещё на 32-битном Office 2016, часть уже на 64-битном Microsoft 365, а тот же .bas иногда подключается в проект VB6. Для этого есть константы условной компиляции VBA7 и Win64.

VBA7 истинна в Office 2010 и новее независимо от разрядности, Win64 истинна только в 64-битном Office. В VB6 обе константы не определены, поэтому компилятор берёт ветку #Else. Неактивные ветки редактор подсвечивает красным, но в сборку они не попадают, и это нормально.

modApi.basVBA
#If VBA7 Then
    ' Office 2010+ (32 и 64 бита)
    Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
        (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
    #If Win64 Then
        Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongPtrA" _
            (ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
    #Else
        Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongA" _
            (ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
    #End If
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
        (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr)
#Else
    ' VB6 и Office до 2010
    Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
        (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" _
        (ByVal hWnd As Long, ByVal nIndex As Long) As Long
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
        (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
#End If
Функция GetWindowLongPtrA экспортируется из user32.dll только в 64-битной Windows для 64-битных процессов, в 32-битном процессе это макрос заголовочного файла. Поэтому 32-битная ветка ссылается на GetWindowLongA, а не на версию с Ptr.

Указатели, структуры и LongLong

Тип LongPtr не самостоятельный: в 32-битном Office он превращается в Long, в 64-битном в LongLong. Сам LongLong существует только в 64-битной сборке, и код, который объявляет переменные As LongLong без проверки #If Win64, не скомпилируется в 32-битном Office.

Самые коварные ошибки сидят в пользовательских типах, которые передаются в API. Структуры вроде OPENFILENAME, BROWSEINFO или NOTIFYICONDATA содержат дескрипторы и указатели, и в 64-битном процессе их размер и выравнивание меняются. Если поле hwndOwner осталось Long, все следующие поля съезжают, а функция возвращает ошибку или портит память.

Отдельно проверьте все места, где размер структуры передаётся вручную числом. Константа 76, честно посчитанная когда-то для 32 бит, в 64-битном процессе неверна. Вычисляйте размер через LenB(переменная), а функции VarPtr, ObjPtr и StrPtr в VBA 7 возвращают LongPtr, поэтому результат нужно хранить в переменной того же типа.

  • найдите все Declare поиском по проекту и заведите список функций с исходными сигнатурами из документации;
  • отдельно выпишите Type, которые уходят в API, и сверьте каждое поле;
  • замените жёстко заданные размеры на LenB;
  • прогоните макрос на обеих разрядностях, а не только скомпилируйте его.

Что не исправить правкой объявлений

Процесс одной разрядности не может загрузить внутрипроцессный компонент другой разрядности. Отсюда целый класс поломок, где PtrSafe не поможет. COM-надстройки и ActiveX DLL, собранные на VB6, всегда 32-битные и в 64-битный Excel не загрузятся. То же касается OCX на пользовательских формах: в 64-битном Office нет MSCOMCTL.OCX, поэтому TreeView, ListView, ProgressBar и ImageList из Windows Common Controls 6.0 исчезают с форм, а календарь MSCAL.OCX отсутствует ещё с Office 2010.

Доступ к данным ломается похожим образом. Старый провайдер Microsoft.Jet.OLEDB.4.0 существует только в 32-битном варианте, в 64-битном Office нужно использовать Microsoft.ACE.OLEDB.12.0 или 16.0 соответствующей разрядности. Обратная ситуация тоже частая: 32-битной программе на VB6 нужен 32-битный ACE, а на машине с 64-битным Office штатный установщик Access Database Engine другой разрядности ставиться отказывается.

Если VB6-программа управляет Excel или Word через CreateObject, она продолжит работать с 64-битным Office: приложение Office запускается отдельным процессом, и вызовы идут через межпроцессный COM. Проблемы начинаются только там, где компонент загружается внутрь процесса.

Как планировать переход

Начните с инвентаризации: соберите все книги с макросами и надстройки, найдите в них Declare, ссылки на OCX (в разделе References и на формах) и строки подключения с Jet. Этот список сразу делится на две группы: то, что лечится правкой кода, и то, что требует замены компонента.

Для первой группы хватит аккуратной работы по чек-листу выше. Для второй варианты такие: заменить контролы стандартными элементами MSForms или вынести интерфейс из Office, переписать COM-надстройку VB6 на .NET с регистрацией в обеих разрядностях, а доступ к данным перевести на ACE нужной разрядности или на внешний сервис. Пока работа идёт, держите 32-битный Office на тех рабочих местах, где без старых надстроек не обойтись, и фиксируйте это в документации к приложению.

Как это делают сегодня

Для новой автоматизации Office вместо Windows API в VBA стоит смотреть на надстройки Office Add-ins на JavaScript, которые не зависят от разрядности, или на VSTO и COM-надстройки на .NET Framework, собираемые под AnyCPU. Если логика тяжёлая, её удобно вынести в отдельный .NET-сервис и вызывать из макроса по HTTP.

Вопросы и ответы

Достаточно ли дописать PtrSafe ко всем Declare?

Нет. Код скомпилируется, но параметры-указатели и дескрипторы, оставшиеся Long, приведут к неверным результатам или падению Office. Типы нужно проверить по документации каждой функции.

Как узнать разрядность Office из макроса?

Проверьте константу #If Win64 в коде. В интерфейсе это видно в разделе Файл, Учётная запись, О программе: там указано «32-разрядная» или «64-разрядная».

Будет ли VB6-программа работать с 64-битным Excel?

Да, если она управляет Excel через CreateObject или GetObject. Не будет работать загрузка 32-битных DLL и OCX внутрь самого Excel.

Читайте также