Как мы встроили 32-битный Internet Explorer в 64-битный Яндекс Браузер для организаций
- четверг, 5 июня 2025 г. в 00:00:10
Многие организации с богатой историей всё ещё завязаны на устаревшие внутренние системы, которые работают исключительно в Internet Explorer (IE). Проблема касается не только внешнего вида, но и самой логики работы: раньше вычисления и ключевые процессы часто реализовывались через отдельный бинарный модуль (чаще всего ActiveX), написанный на компилируемом языке. Этот модуль загружался с сайта на компьютер пользователя и выполнялся внутри процесса браузера.
Шли годы, технологии развивались, компании‑разработчики закрывались, а системы оставались. Организации оказались один на один с legacy‑решениями, которые никто больше не поддерживает и не развивает. Полностью переписывать такие решения — дорого, долго и особенно болезненно в финансовом секторе, где стабильность важнее модных фреймворков.
В результате сотрудники пользуются разными браузерами: одни нужны для современных сервисов, другие — для критически важных старых систем. Разработчикам приходится поддерживать их совместимость, искать хаки и тратить ресурсы на то, чтобы эти системы просто продолжали работать.
В статье расскажем, как Яндекс Браузер для организаций помогает избавиться от этой головной боли и выиграть время для перехода на более актуальные технологии. С его помощью можно запускать современные веб‑приложения и наследие эпохи IE — всё в одном окне. А ещё рассмотрим проблемы, с которыми могут столкнуться специалисты, работающие с legacy‑технологиями, и предложим способы их решения.
Разберёмся, почему некоторые корпоративные сайты до сих пор не работают в современных браузерах и что с этим делать, почему нельзя просто взять и «открыть старое в новом» и какие технические сложности при этом возникают.
До 2010 года Internet Explorer был браузером номер один. Он был предустановлен практически на каждом компьютере, и большинство корпоративных порталов создавалось исключительно под него. По мере набора популярности, IE стал отходить от стандартов, изобретая свои расширения. Когда Firefox и Chromium‑браузеры стали набирать популярность, на первый план вышла проблема, что большинство старых сайтов отображаются в них некорректно из‑за особенностей движка IE:
IE по‑своему считал ширину и высоту элементов с border
и padding
;
отступы во float
‑элементах иногда удваивались;
JavaScript в IE работал со своими нюансами (особенная обработка событий, ActiveXObject и т. д.).
Случалось, что сайт вроде бы работает, но только в одном браузере, и руководство не спешит его переписывать. Проходит время — документация теряется, специалисты уходят, и переписать сайт становится почти невозможно.
Отображение HTML и JS ещё можно починить: включить в браузере режим совместимости, подменить User-Agent
, добавить специфичные свойства в window.navigator
. Но вот <object>
— другая история.
Раньше этот тег мог запускать что угодно — достаточно было указать GUID и ссылку на загрузку. Исполняемый файл загружался и отображался прямо на странице. Это технология ActiveX. Она позволяет создавать мультимедиа, игры, подключаться к базам данных, читать файлы и использовать весь арсенал Windows. без песочниц и ограничений, ActiveX‑компоненты имели полный доступ к ресурсам — даже к памяти самого браузера.
HTML того времени был ограничен, не было элементов canvas, а динамическая догрузка (ajax, XMLHttpRequest) не была распространена, и во многих компаниях пошли по пути создания собственного ActiveX‑компонента, который используется и сейчас. Хотя стандарт object
задумывался как кроссплатформенный, на практике это была 32-битная Windows‑библиотека, без шансов на запуск в Linux или Mac.
Реализация поддержки object равносильна самостоятельной реализации всего COM, поэтому для режима совместимости мы решили не переизобретать технологии 2000-х, а воспользоваться ими: встроить существующий движок Internet Explorer в Яндекс Браузер. Эту задачу мы разделили на три этапа.
Сначала мы сделали отдельное окно, в котором размещался движок IE через IWebBrowser2
из ieframe.dll
, и добавили базовые элементы управления: назад, вперёд, обновить, заголовок страницы, закрыть и т. д.
Когда пользователь переходит по ссылке, браузер проверяет, нужно ли открывать её в режиме совместимости (согласно настройкам групповой политики BrowserSwitcherUrlList
). Если да, то основная вкладка прерывает навигацию и ссылка открывается в отдельном IE‑окне.
В отличие от классического IE‑окна, кнопки остановки/обновления объединены. Когда страница загружается, кнопка остановит загрузку. Если страница загружена, то обновит (как в браузере). Кнопка «Вперёд» не отображается, если навигации назад не было. Такое поведение является стандартом для Chromium‑браузеров.
Работу с куки как с необходимым механизмом для работы сайтов пришлось реализовать с нуля. Чтобы сайты работали корректно, нам нужно было синхронизировать куки Яндекс Браузера (куки‑хранилище там называется CookieMonster, как персонаж Коржик из «Улицы Сезам») и встроенного Internet Explorer (куки, передаваемые в компонент, не сохраняются по завершении сессии). Это оказалось непросто: интерфейс хранения куки асинхронный, а IE требует их синхронно во время навигации. В итоге, чтобы просто начать навигацию, нам пришлось писать асинхронный код: сначала идём в куки‑хранилище и только потом стартуем навигацию.
Вторая проблема — IWebBrowser не уведомляет о том, что куки поменялись. Мы проверяли это, анализируя DOM напрямую.
Сайты, открытые в IEWindow, добавляются в общую историю браузера (browser://history
). Если в таком окне создаётся новая вкладка, которая не требует совместимости, она открывается уже в обычном движке.
В современных браузерах ошибки обработки JavaScript не показываются пользователю — они доступны в логах инструментов разработчика, в то время как компонент IE отображает их все в раздражающих всплывающих окнах. Их полное отключение через put_Silent(true)
не подошло — пропадали и важные уведомления (например, об ошибках сертификата). Поэтому мы добавили фильтрацию через IOleCommandTarget
.
Основная цель этого этапа — удостовериться, что всё работает, сайты и компоненты загружаются и отображаются без проблем.
На втором этапе мы встроили Internet Explorer прямо в окно браузера — так, чтобы пользователь видел его как обычную вкладку. Звучит просто, но на деле мы столкнулись со множеством нюансов.
Во‑первых, IE — это отдельное HWND
‑окно. Современные браузеры рисуются напрямую через DirectComposition
, без оконной иерархии, ради производительности. А любое вложенное окно ломает эту оптимизацию. Пришлось отключать аппаратную композицию, когда появляется IE‑вкладка. Для нормального сценария оптимизация работает.
Во‑вторых, некоторые браузерные меню, например контекстное меню или выпадашки из адресной строки, стали отображаться под IE‑окном. Чтобы не переписывать их отрисовку, мы временно скрываем IE‑компонент, пока пользователь взаимодействует с такими элементами.
Методы интерфейса IE не всегда работали так, как мы ожидали. Так для части вызовов после получение ошибок при их исполнении приходилось делать retry
или изобретать что еще. Например для такого критичного как put_Visible(false)
, где при переключении на другую вкладку мы точно ожидаем бесшовного синхронного переключения, мы пришли к выводу, что самым надежным, будет просто полностью скрывать hwnd
самим. Кроме того, часть критически важных методов периодически завершалась с ошибками. Там, где не удавалось найти элегантный обход, мы вынужденно прибегали к повторным вызовам с небольшой задержкой — и это, как ни странно, помогало.
Чтобы всё выглядело нативно, мы связали браузерные кнопки (назад, вперёд, обновить, сохранить, печать) с действиями IE. Так же отображаются URL, заголовок страницы и иконка — как на обычной вкладке.
Для этого мы внедрили переопределяющий интерфейс в WebContents
, который перехватывает вызовы и делегирует действия IE‑компоненту.
Многие сочетания клавиш, такие как Ctrl+C, Ctrl+V и Ctrl+F, IE уже обрабатывает. Но часть браузерных горячих клавиш перестала работать: они не доходили до акселератора. Для решения этой проблемы мы написали специальный цикл обработки сообщений, который фильтрует нажатия клавиш, и, если это наше особое сочетание, оно обрабатывается в цикле, без DispatchMessage.
Ещё одной интересной особенностью оказалось то, что JavaScript может создавать всплывающие окна, которые остаются связанными с родительским и могут передавать данные обратно. Например, на сайте может открываться календарь в попапе, и после выбора даты он возвращает её в родительское окно.
Перед созданием таких окон IE отправляет уведомление DISPID_NEWWINDOW3
.
Чтобы реализовать корректную связь между окнами, нам пришлось докопаться до того, что в качестве четвёртого параметра необходимо передать указатель на связующий интерфейс IDispatch:
IDispatch** dest = V_DISPATCHREF(¶ms->rgvarg[4]);
Именно это и обеспечивает нужное взаимодействие между окнами.
Если говорить упрощённо, Internet Explorer сам по себе COM‑объект, который отображает сайты внутри окна Windows (HWND
). HWND
— просто дескриптор, с помощью которого мы можем разместить и спозиционировать окно IE внутри нашего интерфейса.
Как мы уже знаем, сайты часто используют устаревшие компоненты — корпоративные COM‑объекты или ActiveX. Общая черта всех этих компонентов: они реализованы в виде 32-битных DLL.
Вот в чём основная проблема: в 64-битном процессе невозможно загрузить 32-битную DLL. Это фундаментальное ограничение. Поэтому при переходе браузера на 64-битную архитектуру такая legacy‑совместимость перестаёт работать.
Мы нашли способ обойти это ограничение. Теперь даже 64-битный Яндекс Браузер может запускать старые сайты с 32-битными компонентами. Это особенно важно для корпоративных клиентов: им не нужно переписывать устаревшие системы — можно просто работать.
Ключ к решению — понимание, как именно создаётся COM‑объект браузера Internet Explorer.
Хорошая новость: в Microsoft реализован COM‑объект браузера Internet Explorer (интерфейс IWebBrowser2) как в 32-битной, так и в 64-битной версиях DLL. Плохая новость: в виде EXE‑файла (внешнего процесса) его нет.
Почему это важно? Посмотрим на типичный код создания IE‑объекта в C++:
Microsoft::WRL::ComPtr<IOleObject> iole;
HRESULT hr = CoCreateInstance(CLSID_WebBrowser, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&iole));
if (FAILED(hr)) {
LOG(FATAL) << "CoCreateInstance: " << _com_error(hr).ErrorMessage();
}
Разберём, что здесь происходит:
CLSID_WebBrowser — это глобальный идентификатор объекта. Он зарегистрирован в системе и соответствует значению {8856F961-340A-11D0-A96B-00C04FD705A2}
(можно найти в заголовке Exdisp.h
).
CLSCTX_INPROC_SERVER означает, что объект будет создан внутри текущего процесса и загрузится как обычная DLL.
То есть CoCreateInstance
находит этот CLSID в реестре, смотрит путь к DLL (в ветке InProcServer32
) и загружает её. В случае IE это ieframe.dll
.
Кажется логичным попробовать другой флаг:
HRESULT hr = CoCreateInstance(CLSID_WebBrowser, nullptr, CLSCTX_LOCAL_SERVER,
IID_PPV_ARGS(&iole));
CLSCTX_LOCAL_SERVER
означает, что объект должен запускаться в отдельном EXE‑процессе. Это как раз то, что нужно: браузер работает в 64 битах, а IE запускается как 32-битный процесс.
Но увы, в реестре у CLSID_WebBrowser нет записи LocalServer32
. Есть только запись InProcServer32
, которая указывает на DLL. То есть Internet Explorer не работает как отдельный EXE‑сервер — в Microsoft не предусмотрено такого сценария.
Поскольку у Microsoft нет готового способа, нам пришлось реализовать собственный процесс — yandex_browser_ie_proxy32.exe
. Это лёгкий 32-битный EXE, который умеет:
запускаться из 64-битного браузера;
создавать нужный 32-битный COM‑объект (IWebBrowser2);
проксировать вызовы от 64-битного браузера к 32-битному процессу.
Так мы получаем полноценную поддержку Internet Explorer даже в 64-битной версии Яндекс Браузера — со всеми ActiveX, COM и остальной корпоративной экзотикой.
Первое, что мы решили проверить: поддерживает ли интерфейс IOleObject стандартный маршалинг? Именно этот интерфейс стоит запросить первым при работе с COM‑объектом WebBrowser.
Под стандартным маршалингом мы имеем в виду простой сценарий: в 32-битном процессе вызываем CoMarshalInterface
, а в 64-битном — CoUnmarshalInterface
. Без необходимости написания собственного Proxy/Stub или регистрации сторонних маршалеров.
К нашему счастью, IOleObject
это поддерживает, что сильно упростило архитектуру взаимодействия 32- и 64-битных процессов.
Давайте попробуем разобраться, что он собой представляет:
HRESULT CoMarshalInterface( | HRESULT CoUnmarshalInterface(
[in] LPSTREAM pStm, | [in] LPSTREAM pStm,
[in] REFIID riid, | [in] REFIID riid,
[in] LPUNKNOWN pUnk, | [out] LPVOID *ppv
[in] DWORD dwDestContext, | };
[in, optional] LPVOID pvDestContext, |
[in] DWORD mshlflags |
);
Первым параметром функции CoMarshalInterface
передаётся указатель на COM‑объект, реализующий интерфейс IStream
. Именно в этот поток будут записаны все необходимые данные, чтобы в другом процессе можно было создать прокси‑объект.
Далее в 64-битном браузерном процессе мы вызываем CoUnmarshalInterface
, передаём ему IStream
с теми же данными, и он на его основе создаёт прокси‑объект, через который можно взаимодействовать с оригинальным 32-битным WebBrowser.
Но здесь возникает вопрос: как передавать этот IStream
между процессами? Начинаешь читать документацию, и первое, что бросается в глаза, это:
HRESULT CreateStreamOnHGlobal(
[in] HGLOBAL hGlobal,
[in] BOOL fDeleteOnRelease,
[out] LPSTREAM *ppstm
);
Сначала у нас была надежда, что HGLOBAL
— это процессо‑независимый дескриптор, наподобие HWND
, но увы, нет. Это ещё одно наследие ранних версий Windows, которое уходит корнями во времена 16-битной архитектуры.
На практике HGLOBAL
— это скорее про создание потокобезопасного буфера внутри одного процесса, а не про удобную передачу данных из одного процесса в другой.
Поэтому мы решили: зачем усложнять? Проще и надёжнее всего использовать привычный всем буфер в виде массива байтов.
IStream * SHCreateMemStream(
[in, optional] const BYTE *pInit,
[in] UINT cbInit
);
Здесь как раз удобно использовать IStream
, созданный на базе обычного массива байтов: мы можем инициализировать его данными в 32-битном процессе, а затем передать этот буфер в 64-битный процесс.
Если же в 32-битном процессе при создании IStream
передать в качестве первого параметра nullptr, то он будет создан без начального содержимого — просто как пустой поток.
Пример создания IStream
и записи в него данных в 32-битном процессе:
Microsoft::WRL::ComPtr<IStream> stream(SHCreateMemStream(nullptr, 0));
hr = CoMarshalInterface(
stream.Get(),
IID_IOleObject,
iole.Get(),
MSHCTX_LOCAL,
nullptr,
MSHLFLAGS_NORMAL);
if (FAILED(hr)) {
LOG(FATAL) << "CoMarshalInterface: " << _com_error(hr).ErrorMessage();
}
// Read marshal data from mem stream.
stream->Commit(STGC_DEFAULT);
LARGE_INTEGER l;
l.QuadPart = 0;
stream->Seek(l, STREAM_SEEK_SET, nullptr);
char buffer[kBufferSize];
ULONG bytes_read = 0;
stream->Read(&buffer, kBufferSize, &bytes_read);
if (bytes_read == 0) {
LOG(FATAL) << "Cannot read marshal data";
}
if (bytes_read == kBufferSize) {
LOG(FATAL) << "Buffer too small for read marshal data";
}
В 64-битном процессе достаточно его просто проинициализировать:
std::string marshal_data(buffer, bytes_read);
Microsoft::WRL::ComPtr<IStream> stream(SHCreateMemStream(
reinterpret_cast<BYTE*>(marshal_data.data()),
marshal_data.size()));
А сколько же байтов мы передаём из одного процесса в другой при маршалинге и демаршалинге? Это стало для меня настоящим сюрпризом — меньше 100 байт! Столь компактный размер данных говорит о том, что COM‑инфраструктура работает действительно эффективно.
С первым параметром (LPSTREAM
) мы разобрались. Теперь кратко пройдёмся по остальным:
Второй параметр — REFIID
: это стандартная для COM ссылка на IID
нужного интерфейса. У каждого COM‑интерфейса есть свой уникальный IID
, который, по сути, представляет собой GUID
(например, тот, который мы ранее упоминали для IWebBrowser2
).
Третий параметр — LPUNKNOWN
: указатель на COM‑объект, который мы хотим замаршалить.
Четвёртый параметр — DWORD
(контекст маршалинга): указывает, где именно будет использоваться замаршаленный интерфейс. В нашем случае мы передаём MSHCTX_LOCAL
, что означает «в другом процессе, но на том же компьютере».
Пятый параметр — зарезервирован, его можно спокойно устанавливать в nullptr
.
Шестой параметр — флаги маршалинга: здесь нас интересует базовый вариант MSHLFLAGS_NORMAL
, когда интерфейс будет демаршалиться только один раз.
Теперь наша задача свелась к довольно понятной цели — передать обычный буфер байтов между процессами. Один из удобных способов сделать это в WinAPI — использовать именованные каналы (Named Pipes).
Самое забавное, что работа с Pipe
почти не отличается от работы с обычными файлами. Из особенностей стоит отметить лишь одно: имя канала должно соответствовать паттерну \\.\pipe\pipename
, тогда серверный процесс (64-битный браузерный) перед началом работы вызывает CreateNamedPipe
:
std::wstring pipe_name = GenerateRandomPipeName();
const DWORD kOpenMode =
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED;
const DWORD kPipeMode =
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT;
base::win::ScopedHandle pipe_handle(::CreateNamedPipeW(
pipe_name.c_str(), kOpenMode, kPipeMode,
1, // Max instances.
kBufferSize, // Out buffer size.
kBufferSize, // In buffer size.
5000, // Timeout in milliseconds.
nullptr));
А клиентский (наш 32-й) прокси‑процесс осуществляет коннект, используя CreateFile
:
// Connect to pipe from command_line.
auto pipe_name = GetPipeNameFrom(command_line);
base::win::ScopedHandle pipe_handle(CreateFile(
pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING, 0, nullptr));
На этом всё, теперь процессы могут обмениваться любыми данными простой парой вызовов WriteFile/ReadFile
:
WriteFile(pipe_handle.get(), &buffer, byte_to_write, &byte_written, nullptr); и ReadFile(pipe_handle.get(), buffer, kBufferSize, &read_bytes, nullptr);
Я сознательно упростил пример — всё будет работать в синхронном режиме. Но всё же считаю важным упомянуть, что в WinAPI есть возможность реализовать это асинхронно.
Для этого существует структура OVERLAPPED
. Её можно передать в качестве одного из параметров в функциях WriteFile
и ReadFile
.
Ключевой момент: внутри OVERLAPPED
есть поле HANDLE hEvent;
, которое можно использовать для отслеживания завершения операций через WaitForMultipleObjects
.
Internet Explorer работает внутри 32-битного процесса — по сути, это окно (hwnd
), созданное в этом же процессе. А значит, все сообщения, связанные со взаимодействием с веб‑страницей (например, когда она в фокусе), будут приходить именно в наш прокси‑процесс. И чтобы они обрабатывались корректно, прокси обязан иметь свой собственный цикл обработки сообщений.
Но браузер — это не просто страница. У него есть привычное поведение: горячие клавиши, открытие новых вкладок, переключение между ними, закладки и т. д. Всё это реализовано уже в основном, 64-битном браузерном процессе. Получается, часть сообщений мы должны обрабатывать в прокси, а часть — пересылать в браузер, в его собственное окно.
На помощь приходит архитектура окон в WinAPI. Дело в том, что HWND
— это процессо‑независимый дескриптор окна. Мы можем спокойно отправлять сообщения из одного окна в другое, даже если они находятся в разных процессах. Более того, численное значение HWND
— общее для всех процессов. Да, в 64-битной сборке HWND
формально 64-битный, но по сути это всё равно 32-битный дескриптор, и, чтобы передать его из одного процесса в другой, достаточно просто передать его значение — например, как параметр командной строки или через Pipe.
HWND GetBrowserHWND(const base::CommandLine* command_line) {
if (command_line->HasSwitch(
internet_explorer_proxy::switches::kBrowserHwnd)) {
std::string hwnd_str = command_line->GetSwitchValueASCII(
internet_explorer_proxy::switches::kBrowserHwnd);
size_t hwnd_value;
if (!base::StringToSizeT(hwnd_str, &hwnd_value)) {
return nullptr;
}
return reinterpret_cast<HWND>(hwnd_value);
}
return nullptr;
}
Для создания HWND
‑прокси окна мы пошли полностью стандартным путём: зарегистрировали простейший класс окна с базовой функцией обработки сообщений:
LRESULT CALLBACK ProxyWndProc(HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam) {
switch (message) {
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, message, wparam, lparam);
}
HWND CreateProxHWND() {
HINSTANCE hinst = GetModuleHandle(nullptr);
std::wstring class_name(L"YandexBrowserProxy32MessageClass");
WNDCLASS wc = {0};
wc.lpfnWndProc = ProxyWndProc;
wc.hInstance = hinst;
wc.lpszClassName = class_name.c_str();
RegisterClass(&wc);
return = CreateWindow(class_name.c_str(), nullptr, 0, 0, 0, 0, 0,
HWND_MESSAGE, nullptr, hinst, nullptr);
}
Но, как позже выяснилось, мы упростили слишком сильно. Встраивание объекта Internet Explorer у нас шло через архитектуру OLE, и реализация ключевых интерфейсов — IOleClientSite
, IOleInPlaceSite
, IOleInPlaceFrame
— находилась в браузерном процессе.
Одним из первых багов, который вскрылся на тестировании, была неработающая печать. Диалог Print Preview открывался, но оставался полностью пустым.
Ключ к проблеме оказался именно в реализации этих интерфейсов:
// IOleWindow
HRESULT STDMETHODCALLTYPE GetWindow(HWND* phwnd) override;
// IOleInPlaceSite
HRESULT STDMETHODCALLTYPE
GetWindowContext(IOleInPlaceFrame** ppFrame,
IOleInPlaceUIWindow** ppDoc,
LPRECT lprcPosRect,
LPRECT lprcClipRect,
LPOLEINPLACEFRAMEINFO info) override;
Несмотря на то что методы были реализованы в браузерном процессе, возвращаемым hwnd
должен был быть hwnd
, созданный в прокси‑процессе, а само это hwnd
необходимо было как child‑окно для браузерного hwnd
:
CreateWindow(class_name.c_str(), nullptr, WS_CHILD, 0, 0, 0, 0, browser_hwnd, nullptr, hinst, nullptr);
Заканчивая рассказ про обработку сообщений, хочу заметить, что в OLE за работоспособность хоткеев отвечает IOleInPlaceActiveObject
и его метод TranslateAccelerator
. Итак, представляю вам пример нашего прокси‑приложения:
int wmain(int, wchar_t*) {
// Read browser_hwnd from command_line.
// Connect to pipe from command_line.
// Create our message Window.
// Send our hwnd as pipe message.
// Initialize COM Library.
// Create IOle object.
// Marshal IOle object into mem Stream.
// Read marshal data from mem stream.
// Send marshal data as pipe message.
// Message loop.
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
if (IsAccelerator(msg)) {
if (IsBrowser(msg)) {
PostMessage(g_browser_hwnd, msg.message, msg.wParam, msg.lParam);
} else {
MSG mutable_msg = msg;
ole_inplace->TranslateAccelerator(&mutable_msg);
}
continue;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return ERROR_SUCCESS;
}
Хочу подытожить статью примером того, насколько важны детали, когда работаешь с устаревшей технологией. Иногда одной строки кода может не хватить — и что‑то не заработает, начнёт вести себя странно или будет работать не так, как ожидается. После этого начинается череда экспериментов и пересборок, отладка, чтение мануалов и форумов. Это может занять часы, а то и дни.
Зададимся простым вопросом: если мы хотим начать работать с COM, то как его инициализировать?
Ответ вроде бы очевиден — вот такая строка:
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) {
LOG(FATAL) << "CoInitializeEx: " << _com_error(hr).ErrorMessage();
}
В первоначальной версии кода мы именно так и поступили — казалось бы, что может пойти не так? Объекты создаются, данные передаются, страницы загружаются — всё работает. Остаётся только протестировать, найти баги и раскатить в прод.
Отдаём в тестирование — и возникает один из первых критичных багов: на веб‑страницах не работает базовая функциональность Copy/Paste. Я не буду подробно рассказывать, сколько отладочной информации мы добавили, сколько раз пытались дебажить очевидные места вроде вызова TranslateAccelerator
у объекта IOleInPlaceActiveObject
. Сразу напишу ответ: мы неправильно инициализировали COM. Точнее, не сам COM, а OLE.
В документации к OleInitialize
чёрным по белому написано:
«Приложения, использующие следующие функции, должны вызывать
OleInitialize
перед вызовом любой другой функции библиотеки COM: Clipboard, Drag and Drop, Object Linking and Embedding (OLE), In‑place Activation».
То есть OleInitialize внутри себя вызывает CoInitializeEx
с параметром COINIT_APARTMENTTHREADED
(так как OLE‑операции не потокобезопасны и требуют однопоточной модели), но дополнительно проводит инициализацию, без которой часть функциональности попросту не будет работать, и при этом не выдаёт никаких явных ошибок.
Поддержка 32-битного Internet Explorer в 64-битном Яндекс Браузере для организаций — это не просто про «встроить старый движок». Это про хрупкий баланс между COM‑архитектурой, маршалингом, pipe, hwnd и OLE‑инициализацией. Это про необходимость буквально вручную прокладывать мост между двумя архитектурами, учитывая каждую мелочь и каждый системный нюанс.
Я старался показать в статье не только архитектурные решения, но и типичные грабли, на которые легко наступить, если недооценить возраст и особенности технологий, с которыми приходится работать. Где‑то всё упиралось в одну строчку кода, где‑то — в десятилетние особенности WinAPI, COM или реестра.
Наша задача была не просто заставить legacy‑контент отобразиться, а сделать этот процесс безопасным, стабильным и прозрачным для пользователя. И если эта статья кому‑то поможет избежать лишней отладки, бессонных ночей или хотя бы просто сохранить веру в здравый смысл — значит, всё не зря.