Борьба бобра с ослом, или Адаптация MSVC кода под gcc

    Статья описывает некоторые затруднения, которы мы встретили при попытке адаптации одного из наших старых Windows-only проектов (плагин к MT4 серверу) к кросскомпиляции под Linux (CI, статический анализ, автотесты и прочие модные слова). Точнее, в коде присутствовал ряд конструкций, которые спокойно съедались MSVC, но категорически отказывались компилироваться с использованием mingw/gcc.


    image


    Под катом 7 наиболее часто встретившихся примеров кода, которые будут компилироваться MSVC, но не будут с gcc, и способы это лечить.


    Дисклеймер


    Цель статьи – не сказать, что какой-то компилятор лучше, чем другие, а указать на некоторые проблемы, которые могут возникнуть при адаптации кода к другим компиляторам (особенно если до этого использовался только MSVC). Также некоторые (если не все) элементы поведения можно свести к одному, если подкрутить флаги компиляции, но ведь лучше все-таки поправить код (хотя бы и sed'ом), правда?


    Если вы начинающий трейер, то предлагаем вам почитать тут.


    Условия задачи


    Имеем среднего размера проект (около 15к SLOC не считая библиотек), в котором используется CMake с практически дефолтными флагами компиляции. MSVC используем 14 версии, а mingw-gcc — 6.3.


    Найденные проблемы


    Декорация имен методов


    Внутри нашего проекта присутствует несколько методов, которые должны вызываться как C методы для того, чтобы плагин распознавался сервером. Оригинально в коде использовались следующие конструкции:


    __declspec(dllexport) void SomeMethod() {}

    При компиляции gcc имя функции декорировалось, что приводило к тому, что сервер не определял метод в плагине. Более правильное (рабочее) решение:


    extern "C" __declspec(dllexport) void SomeMethod() {}

    Пути к файлам и include


    Различие разделителей в путях на разных системах также приводит к ошибкам на этапе компиляции. Код


    #include "directory\\include.h"

    откажется компилироваться под Linux/gcc, хотя под Windows/MSVC никаких проблем не будет. Это не совсем ошибка, но следует отметить, что для удобства переносимости лучше все же использовать обычный слэш, поскольку он воспринимается большинством систем. С путями также есть и другая проблема...


    Регистр и include


    Как вы, вероятно, знаете, пути some/path и SoMe/pATh в Windows не различаются, но это не так в некоторых других системах, что приводило к ошибкам, если программист указывал путь в заголовочному файлу без учета регистра. Например:


    #include <Winsock2.h>

    выдаст ошибку с gcc под Linux, потому что указанный файл просто не будет найден. Аналогичная проблема также наблюдается с именами библиотек, например, Ws2_32 против ws2_32.


    Как определить целевую платформу


    В проекте активно используется QuickFIX, который, как предполагается, должен компилироваться и работать под разными системами. В актуальной версии QuickFIX используются следующие конструкции:


    #ifndef _MSC_VER
    #include <unistd.h>
    #endif

    Не надо так делать. При использовании mingw _MSC_VER не определяется, вместо этого правильнее проверять _WIN32 для определения целевой платформы, а _MSC_VER использовать, только если вы хотите включить код, специфичный для MSVC.


    Pure virtual методы


    Код


    class SomeClass
    {
      virtual void someMethod() = NULL;
    };

    при попытке компиляции gcc радостно скажет


    invalid pure specifier (only «= 0» is allowed)

    но не вызовет ошибок у MSVC. Причина проста: gcc раскрывает макрос NULL не в 0, а в __null (что, в общем-то, совсем не запрещено). Решение: очевидно, отказаться от использования NULL для указания pure virtual методов и использовать = 0.


    Определение методов внутри заголовочного файла


    Код


    class SomeClass
    {
      SomeClass::SomeClass() {};
    };

    при использовании gcc выдаст


    extra qualification ‘SomeClass::’ on member ‘SomeClass’

    Правильный ответ, очевидно, не должен содержать SomeClass::. Вообще, в драфте стандарта C++14 (параграф 8.3) написано, что:


    the declaration shall refer to a previously declared member of the class or namespace to which the qualifier refers

    Декларация переменной без указания переменной


    Код, написанный с использованием клипбордного интерфейса


    void someMethod()
    {
      SomeClass;
      SomeClass class;
    }

    содержит в себе ошибку, которая игнорируется MSVC, но вызовет ошибку на этапе компиляции у gcc:


    declaration does not declare anything

    Не уверен, какое поведение здесь правильное, а MSVC, вероятно, просто вырезает неиспользуемую строчку без подачи каких либо сигналов.


    Вместо послесловия


    Большинство перечисленных мной ошибок, очевидно, довольно легко правятся и без чтения данной статьи, руководствуясь исключительно замечаниями компилятора. Однако она наглядно иллюстрирует различие «двух миров» в восприятии вашего исходного кода, и то, что для вас может быть естественным, отнюдь не является таковым при смене компилятора.

    EXANTE
    36,00
    Инвестиционная компания нового поколения
    Поделиться публикацией

    Комментарии 12

      +8
      Более правильное (рабочее) решение:

      extern «C» __declspec(dllexport) void SomeMethod() {}

      А еще более правильное — использовать вместо __declspec(dllexport) макрос, который развернется в __declspec(dllexport)/__declspec(dllimport) на винде в зависимости от того, используется хедер для сборки или линковки библиотеки, или в __attribute__((visibility(«default»))) в gcc
        0

        Лично я вообще удивлён, что кто-то пишет такое напрямую, без макроса. По-моему макрос — это уже стандарт де-факто.

          +3
          костыль это, ставший «стандартом» в силу отсутствия альтернатив
            0

            Тем не менее, это фактический стандарт. Каким бы костыльным он ни был. Как вы сами заметили — альтернатив-то нет. Тем более меня удивляет его отсутствие, тем более в студии, где наверняка это уже где-нибудь в шаблонах кода прописано.

              0
              Есть идеи по добавлению: https://stdcpp.ru/proposals/feb5244f-f6a9-4cc0-ae30-f6b549d2d6c9
        +5

        Ещё MSVC проглатывает отсутствие ключевого слова typename в некоторых случаях, а GCC ругается:


        need ‘typename’ before ‘A<T*>::obj’ because ‘A<T*>’ is a dependent scope

        Например, есть у нас шаблонный класс:


        template<typename T>
        class A
        {
            typedef SmartPointer<A> Pointer;
        };

        И мы его используем где-то в шаблонном классе:


        template<typename T>
        class B
        {
            typename A<T>::Pointer m_A; // MSVC не ругается на отсутствие typename
        };

        MSVC это проглотит без указания typename без всяких предупреждений и это бесит.

          +1
          То же самое с ключевым словом template в аналогичных ситуациях.
          +3

          Еще можно добавить про способы упаковки структур. __attribute__ ((packed)) из gcc, не работет в Visual C++, а аналог делается через pragma. Поэтому в нашем проекте пришлось городить что типа вот этого:


          #        if defined(_MSC_VER)
          /*           __pragma() is specified starting from Visual Studio 2008*/
          #            if (_MSC_VER < 1500)
          #                error "Unsupport Visual C compiler version. Minimum version is Visual Studio 2008."
          #            endif
          #            define ATTRIBUTE_PACKED
          /*           Enable packing and supress warning C4103: Packing was changed after the inclusion of the header, probably missing #pragma pop */
          #            define BEGIN_ATTRIBUTE_PACKED __pragma(pack(push,1)) \
                                                     __pragma(warning(disable : 4103))
          /*           Disable packing and enable warning C4103 back */
          #            define END_ATTRIBUTE_PACKED   __pragma(pack(pop)) \
                                                    __pragma(warning(default : 4103))
          #            define ATTRIBUTE_SECTION_GCC(x)
          #        elif defined (__GNUC__)
          #            define BEGIN_ATTRIBUTE_PACKED
          #            define END_ATTRIBUTE_PACKED
          #            if defined(__clang__)
          #                define ATTRIBUTE_PACKED __attribute__ ((packed))
          #            else
          #                define ATTRIBUTE_PACKED __attribute__ ((gcc_struct,packed))
          #            endif
          #        endif /* defined(_MSC_VER) */

          При это стурктуры потом приходится определять вот таким образом:


              BEGIN_ATTRIBUTE_PACKED
          
              struct s1 ATTRIBUTE_PACKED {
                    ...
              }
          
              struct s2 ATTRIBUTE_PACKED {
                     ...
              }
          
              END_ATTRIBUTE_PACKED
            +1
            Разумнее перейти на alignas() из C++11, если, конечно, есть возможность.
              +2
              к сожалению, не всё так просто. alignas() не позволяет уменьшить выравнивание меньше, чем выравнивание по умолчанию, поэтому добиться с его помощью того же эффекта, что и #pragma pack(push,1) не получится. Более, того, по стандарту new может игнорировать alignas для over-aligned типа (alignof которого выше alignof(std::max_align_t); однако на практике new по умолчанию возвращает память, выровненную по этой планке). В итоге alignas() получается полезен только в очень узком наборе ситуаций.
            0
            Скажите, а зачем понадобилось плагин к mt4 компилировать под Linux?
              0
              у нас все девелоперское окружение (включая всякие jenkins) работают под линуксом, поэтому изначально данный проект собирался разработчиком на личной машине (что, например, мне неудобно, потому что нужно заводить виртуалку) или на тестовом сервере с МТ4 (тоже боль, потому что через какой нить rdp и автоматизация примерно никакая).

              PS речь идет про кросскомпиляцию, очевидно.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое