Międzynarodowa konferencja twórców
i użytkowników Free Software / Open Source (FS/OS)

Кроссплатформенная разработка. Опыт Opera Software.

Алексей Хлебников, Oslo, Norway

LVEE 2015

Cross-platform development gives you more users, less development costs, immunity to vendor lock-in and other goodies. But how to do it properly? Below the experience of Opera Software with its famous Opera browser is presented, including problems that arised and how they were solved.

Зачем нужна кроссплатформенность

Преимущества кроссплатформенной разработки очевидны. Если программа написана под несколько платформ – у неё будет больше пользователей. Затраты на разработку будут гораздо меньше, чем если бы под каждую платформу программа писалась бы с нуля. Разработчик не привязан к единственной платформе, а поэтому приобретает иммунитет к vendor lock-in. Разработка кроссплатформенного приложения заставляет задуматься о хорошем дизайне программы, избегать привязки к “нестандартностям” и недокументированным функциям платформы, что приводит к меньшему числу багов. Есть и рекурсивный аргумент: если программа написана “в кроссплатформенной манере” – добавление поддержки ещё одной платформы происходит гораздо легче.

Платформы, которые поддерживал браузер Opera

Примером ультра-кроссплатформенной программы можно считать (старый)web-браузер Opera на движке Presto. Благодаря высокой кроссплатформенности этого движка, браузер Opera поддерживал следующие платформы:

  • Desktop, в том числе FreeBSD, Solaris, OS/2, BeOS и QNX
  • Планшеты
  • Smartphone, в том числе Symbian/Series60, Maemo/Meego, Bada, BlackBerry, Windows CE/Mobile
  • Feature phone, в том числе BREW, P2K, EPOC, Psion, Java ME
  • Игровые консоли, в том числе Playstation 3, Nintendo Wii и Nintendo DS
  • Smart и не-smart TV, TV-приставки
  • Автомобильные компьютеры
  • Панель управления башенного крана

Пожалуй, Opera будет покроссплатформеннее ядра Linux.

Возникающие проблемы

Конечно, при кроссплатформенной разработке встречаются и трудности. Разные платформы – значит разные устройства, с разными характеристиками:

  • Разные скорости CPU и объёмы памяти
  • Разные размеры экранов и их количество
  • Разные устройства управления
  • Разные дополнительные устройства (камера, GPS, etc)
  • Разные компиляторы, в том числе и капризные (ADS)
  • Особые методы запуска приложений (BREW, P2K)
  • Разные API у разных операционных систем

Требования Opera 8.5x/Presto 1.0 (2005~2011)

Да-да, некоторые производители, например Motorola, поставляли телефоны с браузером Opera Mobile 8.54 вплоть до 2011 года.

Системные требования для Opera 8.5x на движке Presto 1.0 составляли:

  • 32-битный процессор
  • 8 MБ оперативной памяти, доступной для использования Оперой
  • C++-подобный компилятор для платформы

Остальные возможности платформы, даже устройства ввода и вывода, сеть и файловая система, являлись, строго говоря, необязательными. Например, Presto успешно обходилось без устройств ввода-вывода на серверах Opera Mini.

В списке не случайно упомянут “C++-подобный” компилятор. Opera могла быть собрана практически любым компилятором, даже самым капризным.

Как же Опере удавались эти чудеса? Секрет в хорошем проектировании программы.

Модель кроссплатформенного приложения

Архитектура “старой Оперы” представлена на следующей схеме:

Приложение состоит из 3 частей:

  • UI – реализация UI, платформо-зависимый код.
  • Core – ядро/движок, платформо-независимый код.
  • Platform – реализация поддержки файловой системы, сети и устройств, платформо-зависимый код.

Стрелки на схеме означают:

  • CoreAPIUsers → CoreAPI – Команды от UI к Core, например “Перейти на такой-то URL в таком-то табе”.
  • UIEventIssuers → UIEventHandler – События в UI, которые должны быть обработаны Core, например клик мыши.
  • CoreEventIssuers → CoreEventHandler – События в Core, которые должны быть обработаны в UI, например перерисовка области экрана.
  • Some interface → Some implementation – вызовы кода, реализующие “платформенный интерфейс”, то есть отвечающего за поддержку файловой системы, сети и устройств на данной платформе.

Таким образом, для того, чтобы добавить поддержку какой-либо платформы, надо:

  • Решить, какие возможности браузера и устройства хочется поддерживать на этой платформе.
  • Разработать реализацию UI и платформенных интерфейсов, с учётом выбранных возможностей.

То есть, если какая-то возможность браузера (например, поддержка GPS) не будет активирована на данной платформе – не надо и реализовывать платформенный интерфейс GPS-устройства. Подробнее включение/отключение возможностей программы будет описано ниже.

Как решать проблемы

Выше были упомянуты некоторые проблемы, присущие кроссплатформенной разработке. Следующая таблица кратко описывает, как их решать.

Проблема Решение
Разные скорости CPU и объёмы памяти Включение/отключение возможностей на стадии компиляции
Разные размеры экранов и их количество Задание размера экрана/окна при инициализации/изменении
Разные устройства управления Поддержка в UI layer, возможно частично в Platform layer
Разные дополнительные устройства (камера, GPS, etc) Поддержка в Platform layer, обязательный #ifdef SOME_DEVICE_SUPPORT
Разные компиляторы, в том числе и капризные (ADS) Ответственное использование С++ (namespaces, templates, exceptions, RTTI, global vars, static)
Особые методы запуска приложений (BREW, P2K) Реализация UI как библиотеки
Разные API у разных операционных систем Поддержка в Platform layer

Включение/отключение возможностей на стадии компиляции

Некоторые платформы, например feature phones, имеют не очень-то много памяти, как для хранения кода приложения, так и для его работы. Отключение некоторого функционала приложения на стадии компиляции помогает уменьшить размер кода этого приложения, расход памяти, а также ускоряет его работу.

Также, чем менее мощны устройства определённой платформы – тем, как правило, меньший спектр встроенных устройств (камера, GPS, etc) они поддерживают. Код для неподдерживаемых встроенных устройств на данной платформе также следует исключать из компиляции.

Для того, чтобы реализовать отключаемую поддержку определённого функционала или устройств на стадии компиляции для определённой платформы, используется простой способ:

  1. Обрамить код поддержки отключаемого функционала или устройства в “#ifdef SOME_FEATURE_SUPPORT” .
  2. Описывать (#define) или сбрасывать (#undef) соответствующий макрос в глобальном header-файле для соответствующей платформы.

Ниже приведён фрагмент C++ кода, который иллюстрирует описанный способ:

    #define FEATURE_1_SUPPORT
    #undef  FEATURE_2_SUPPORT
        ...
        // Код feature 1 будет скомпилирован,
        // потому что решено поддерживать
        // feature 1 на этой платформе.
    #ifdef FEATURE_1_SUPPORT
        m_component_loader->LoadFeature1Data();
    #endif
        ...
        // Код feature 2 не будет скомпилирован,
        // потому что решено не поддерживать
        // feature 2 на этой платформе.
        // Это уменьшит размер исполняемого кода,
        // потребление памяти во время исполнения
        // и увеличит его скорость.
    #ifdef FEATURE_2_SUPPORT
        m_component_loader->LoadFeature2Data();
    #endif
        ...

Как ещё уменьшить размер кода

Существуют также и другие способы уменьшить размер кода:

  • Использовать код повторно, удалять более неиспользуемый код
  • Уменьшить размер временных буферов и/или их время жизни (совет для runtime)
  • Больше использовать библиотеки платформы, не дублировать их функционал в коде приложения
  • Урезать используемые third-party библиотеки, всё-таки включённые в приложение; таким же образом (#undef FEATURE), как урезается основной код
  • Избегать зависимостей от слабых мест компилятора, (пример: раздувание template-кода)
  • Кросс-компилировать лучшим компилятором
  • Компилировать в более компактный набор инструкций процессора (пример: Thumb для ARM)
  • Сжимать код, в ROM или RAM
  • Использовать плагины/оверлеи
  • Исполнять части кода в другом месте (пример: Opera Mini)

Пример платформенного интерфейса

Ниже приведён пример платформенного интерфейса для TCP сокетa. Реализовав этот платформенный интерфейс, разработчик обеспечит работу TCP соединений на данной платформе. Для Unix-подобной платформы реализация может использовать BSD sockets, для Windows – WinSock, для Telium OS – LinkLayer и т.д.


    // Будет реализовано в платформенном коде.
    class TCPSocket
    {
        static TCPSocket* New();
        Status SetListener(TCPSocketListener* listener) = 0;

        Status Connect(const char* host, unsigned short port) = 0;
        Status Disconnect() = 0;
        Status Send(const char* data, size_t length, size_t& accepted_length) = 0;
        Status Recv(char* data, size_t length, size_t& received_length) = 0;
    };

    // Будет реализовано в коде ядра.
    class TCPSocketListener
    {
        Status OnConnected() = 0;
        Status OnDisconnected() = 0;
        Status OnDataSent(size_t length) = 0;
        Status OnDataAvailable(size_t length) = 0;
        Status OnError(Error code) = 0;
    };

Abstract licensed under Creative Commons Attribution-ShareAlike 4.0 International license

Wstecz