Объедините C и C++ в разработке приложений STM32 [закрыто]

Как правило, разработка приложений для встраиваемых систем основана на абстракции. Мы открываем некоторые функции из нижних уровней, которые можно использовать на прикладном уровне. Мой вопрос: могу ли я разработать прикладной уровень на С++, используя подход POO (классы, наследование и т. д.)? Можно ли совместить C и C++ в одном проекте?

Это действительно слишком широко и основано на мнениях для формата вопросов и ответов.

Ответы (2)

Обзор

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

Позвольте мне обсудить некоторые проблемы, которые следует рассмотреть, предполагая, что вы действительно можете использовать конкретный компилятор C и конкретный компилятор C++ (или использовать один и тот же компилятор с разными параметрами, чтобы он действовал как компилятор C или C++, но не оба) и где компоновщик может нормально читать объектные файлы и будет пытаться разрешать символы и отображать адреса точно для некоторого конкретного целевого устройства.

До достижения main()

Не вдаваясь в подробности, и C, и C++ склонны считать, что они могут предоставить компоновщику "отправную точку" для вашего кода. Вам нужна отправная точка, чтобы при запуске целевой системы в вашем коде было определенное место для начала. Но на самом деле ни C, ни C++ не запускают код с помощью main(). Они начинаются где-то еще, потому что есть много мелких деталей, которые нужно настроить для вас, прежде чем это произойдет. Например, код для начальной точки в C обычно находится в чем-то, что обычно называется «crt0». Здесь находится и устанавливается блок памяти, необходимый для инициализации функций управления кучей, malloc() и free(). Здесь, если необходимо, все ваши статические переменные времени жизни устанавливаются с их начальными значениями (которые, возможно, придется скопировать из флэш-памяти в оперативную память) и т. д. Аналогичные потребности есть и у C++. Но в C++ они отличаются от C. Например, в C++ по умолчанию используется другой диспетчер памяти в куче.

Итак, кто запускает код запуска во время выполнения?? Как компоновщик догадается об этом? Обычно компоновщику предоставляется повторяющаяся запись во всех объектных файлах, в которой снова и снова утверждается одно и то же — ссылка «crt0» (или эквивалентная потребность C++) в проект. Например, именно здесь существует вызов main(). Именно здесь выполняется вся эта инициализация перед вызовом main(). И это обычно обеспечивается через скрытые связи, которые вы не можете контролировать (или, по крайней мере, не очевидным образом).

(Другой вариант этой инициализации, который использовался в первые дни Unix, заключался в том, чтобы исполняемый файл на диске предоставлял необходимые сегменты и позволял загрузчику программы в ОС выполнять эту работу по инициализации. О/С предоставляла кучу непосредственное управление пространством. В таком случае отправной точкой всегда считался первый двоичный байт кодового пространства, найденного в файле.)

Вам нужно будет решить, как все это делается с помощью компоновщика и как это можно переопределить, или как вы могли бы написать свой собственный код 'crt0', который работает с библиотеками как C, так и C++. Также возможно, что инструменты вашего компилятора облегчат вам задачу. Я не могу сказать. Но, по крайней мере, вы должны знать об этом скрытом поведении и иметь план правильного управления им.

Совместимость с библиотекой

Часто это большая проблема. Если ваш код C++ ссылается на библиотечный код, а ваш код C также ссылается на библиотечный код, маловероятно (если только поставщик компилятора не является одним и тем же для обоих компиляторов и работал над достижением этого), что они будут совместимы в своих требованиях к инициализации. Как упоминалось выше, перед запуском функции main() выполняется код инициализации. Но не только код инициализации. Также существует завершающий код (например, в C есть код atexit(), который срабатывает, если ваша программа на C должна завершиться — что обычно не делается во встроенных приложениях, но это не значит, что этого никогда не происходит).

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

Изменение имени C++

C++ прячет свою способность разрешать функции с одинаковыми именами, просматривая типы и номера параметров в чем-то, что обычно реализуется как «искажение имен». Это просто означает, что имя подпрограммы (которое ищет компоновщик) изменено таким образом, что оно включает некоторый "секретный код" как часть своего имени. Этот дополнительный код, добавленный к имени функции, позволяет компоновщику сопоставлять правильные вызовы с правильными подпрограммами.

Но это представляет проблему для C. У вас НЕТ НИКАКОЙ СПОСОБНОСТИ узнать, как компилятор C++ исказил имена. Таким образом, вы не можете знать, как вызывать их из C. Большинство компиляторов C++, предназначенных для многоязыковой компоновки, будут включать какой-либо способ заставить свои функции C++ работать с фиксированным именем. Часто именно здесь вы предоставляете отдельную функцию, скомпилированную компилятором C++, которая выдается как функция интерфейса "C", но где эта функция интерфейса "C" компилируется компилятором C++ и может вызывать функцию C++, которую вы в розыске. Затем компоновщик может проработать остальные детали. Конечно, для этого требуется, чтобы компилятор C++ поддерживал эту идею. Не думаю, что это требование стандарта.

Вызов вызова

Если вы используете разные инструменты компилятора для C и C++, возможно, компиляторы по-разному предполагают, как выполняются вызовы. (Помимо проблемы с искажением имен.) Это предположения о том, как передаются значения параметров (они не обязаны находиться в стеке, поскольку компиляторы могут выбирать альтернативы), какие регистры должны сохраняться при вызове функции, а какие можно поцарапать, как выполняется выделение локальных переменных и так далее. В коде пролога и эпилога каждой скомпилированной функции скрыто множество предположений. И очень важно, чтобы эти вещи были устроены совместимо. Вы не можете предположить, что они будут. Так что вы должны проверить.

Остальная часть истории

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

Я все еще очень осторожно отношусь к C++ при кодировании встроенных приложений. Некоторые причины включают в себя:

  • частичная специализация шаблона
  • виртуальные таблицы
  • виртуальный базовый объект
  • Обработка исключений
  • кадр активации
  • раскрутка кадра активации
  • использование интеллектуальных указателей в конструкторах и почему
  • оптимизация возвращаемого значения

Это только краткий список.

Давайте быстро взглянем на семантику исключений C++, чтобы получить представление.

Компилятор C++ должен генерировать правильный код для единицы компиляции. А когда совершенно не представляет, какая обработка исключений может потребоваться в отдельной единице компиляции Б , составленный отдельно и в разное время.

Возьмите эту последовательность кода, найденную как часть какой-то функции в каком-то блоке компиляции. А :

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

В целях обсуждения блок компиляции А нигде в источнике не используется try..catch . Он также не использует «бросок». На самом деле, допустим, что он не использует исходный код, который не может быть скомпилирован компилятором C, за исключением того факта, что он использует поддержку библиотеки C++ и может обрабатывать такие объекты, как String. Этот код может быть даже файлом исходного кода C, который был немного изменен, чтобы использовать преимущества некоторых функций C++, таких как класс String.

Кроме того, предположим, что foo() — это внешняя процедура, расположенная в модуле компиляции. Б и что у компилятора есть объявление для него, но он не знает его определения.

Компилятор C++ видит первый вызов foo() и может просто разрешить нормальное раскручивание кадра активации, если foo() выдает исключение. Другими словами, компилятор C++ знает, что на этом этапе не требуется никакого дополнительного кода для поддержки процесса раскрутки кадра, связанного с обработкой исключений.

Но как только String s создан, компилятор C++ знает, что он должен быть должным образом уничтожен, прежде чем будет разрешена раскрутка кадра, если позже возникнет исключение. Таким образом, второй вызов foo() семантически отличается от первого. Если второй вызов foo() выдает исключение (которое он может или не может сделать), компилятор должен поместить код, предназначенный для обработки уничтожения String , прежде чем произойдет обычная раскрутка кадра. Это отличается от кода, необходимого для первого вызова foo().

(В C++ можно добавить дополнительные декорации, чтобы уменьшить эту проблему. Но дело в том, что программисты, использующие C++, просто должны гораздо больше осознавать значение каждой строки кода, который они пишут.)

В отличие от malloc C, новый C++ использует исключения, чтобы сигнализировать, когда он не может выполнить необработанное выделение памяти. Так же будет и 'dynamic_cast'. (См. 3-е изд. Страуструпа, Язык программирования C++, стр. 384 и 385 для получения информации о стандартных исключениях в C++.) Компиляторы могут позволить отключить это поведение. Но в целом вы понесете некоторые накладные расходы из-за правильно сформированных прологов и эпилогов обработки исключений в сгенерированном коде, даже если исключения фактически не имеют места и даже когда компилируемая функция фактически не имеет каких-либо блоков обработки исключений. (Страуструп публично сокрушался по этому поводу.)

Без частичной специализации шаблонов (не все компиляторы C++ ее поддерживают) использование шаблонов может привести к катастрофе для встроенного программирования. Без него расцвет кода представляет собой серьезный риск, который может мгновенно убить встроенный проект с небольшим объемом памяти.

Когда функция C++ возвращает объект, создается и уничтожается безымянный временный компилятор. Некоторые компиляторы C++ могут предоставить эффективный код, если в операторе return используется конструктор объекта вместо локального объекта, что сокращает потребность в построении и уничтожении одним объектом. Но не каждый компилятор делает это, и многие программисты на C++ даже не подозревают об этой «оптимизации возвращаемого значения».

Предоставление конструктору объекта единственного типа параметра может позволить компилятору C++ найти путь преобразования между двумя типами совершенно неожиданным для программиста способом. Такое «умное» поведение не является частью C.

Предложение catch, указывающее базовый тип, «разрежет» брошенный производный объект, потому что брошенный объект копируется с использованием «статического типа» предложения catch, а не «динамического типа» объекта. Нередкий источник страданий от исключений (когда вы чувствуете, что можете позволить себе исключения даже во встроенном коде).

Компиляторы C++ могут автоматически генерировать для вас конструкторы, деструкторы, конструкторы копирования и операторы присваивания с непредвиденными результатами. Требуется время, чтобы получить возможность с деталями этого.

Передача массивов производных объектов в функцию, принимающую массивы базовых объектов, редко генерирует предупреждения компилятора, но почти всегда приводит к некорректному поведению.

Поскольку C++ не вызывает деструктор частично сконструированных объектов, когда в конструкторе объектов возникает исключение, обработка исключений в конструкторах обычно предписывает «умные указатели», чтобы гарантировать, что сконструированные фрагменты в конструкторе будут правильно уничтожены, если там действительно возникнет исключение. . (См. Страуструп, стр. 367 и 368.) Это обычная проблема при написании хороших классов на C++, но, конечно, ее избегают в C, поскольку C не имеет встроенной семантики построения и разрушения. Написание надлежащего кода для обработки построения подобъектов внутри объекта означает написание кода, который должен справляться с этой уникальной семантической проблемой на C++; другими словами, "записывать" семантическое поведение С++.

C++ может копировать объекты, переданные в параметры объекта. Например, в следующих фрагментах вызов "rA(x);" может привести к тому, что компилятор C++ вызовет конструктор для параметра p, чтобы затем вызвать конструктор копирования для передачи объекта x в параметр p, а затем еще один конструктор для возвращаемого объекта (безымянного временного) функции rA, что, конечно же, скопировано из параметра p. Хуже того, если у класса А есть свои объекты, которые нужно построить, это может привести к катастрофическим последствиям. (Программист AC мог бы избежать большей части этого мусора, оптимизируя вручную, поскольку программисты C не имеют такого удобного синтаксиса и должны выражать все детали по одному.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

longjmp() в C не имеет переносимого поведения в C++. (Некоторые программисты на C используют это как своего рода механизм «исключения».) Некоторые компиляторы C++ на самом деле пытаются настроить вещи на очистку, когда принимается longjmp, но это поведение не переносимо в C++. Если компилятор очищает сконструированные объекты, он не является переносимым. Если компилятор не очищает их, то объекты не уничтожаются, если код выходит из области действия сконструированных объектов в результате longjmp и поведение является недопустимым. (Если использование longjmp в foo() не выходит за рамки, то поведение может быть нормальным.) Это не слишком часто используется программистами встраиваемых систем C, но они должны знать об этих проблемах, прежде чем их использовать.

Краткое содержание

Вышесказанное — это просто широкое обсуждение. Обработка исключений в C++ требует определенной договоренности с фреймами активации, которые должны каким-то образом включать способы отслеживания и использования обработчиков исключений. C не имеет этого требования для своих фреймов активации. Так что если код C++ вызывается кодом C, а код C++ каким-либо образом также вызывает некоторый код C перед возвратом, то вполне возможно, что последовательность кадров активации будет включать в себя какие-то "недопустимые кадры", которые не могут быть обработаны кодом C++. подпрограммы обработки исключений, используемые C++. А это может привести к фатальным последствиям.

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

Воспитывать себя.

Это отличный ответ!
Здесь смешаны довольно маловероятные вещи. Например, кто-то, кто использует и C, и C++, почти наверняка будет использовать два аспекта одного и того же набора компиляторов. Они спроектированы так, чтобы взаимодействовать довольно хорошо — некоторые из проблем, о которых вы говорите, необходимо иметь в виду, но о других нужно позаботиться, потому что смешанные проекты вполне нормальны. Или, по крайней мере, так же нормально, как встроенный C++ - на самом деле странностью был бы проект встроенного C++, который не включал некоторые исходные коды C.
@ChrisStratton Я испытал все эти случаи, как указано. И более. Некоторые компиляторы сегодня работают как компиляторы C и C++ (например, Visual Studio от Microsoft может делать то же самое, а также инструменты gnu). И я рассматриваю эту возможность в своих работах. Но я не видел, чтобы в OP указывался конкретный набор инструментов, поэтому при написании мне нужно было придерживаться довольно общего подхода. (Правда, я не занимался разработкой STM32. Так что, возможно, это моя ошибка?)
Практически любая текущая цепочка инструментов MCU имеет режимы C и C++, которые предназначены для совместного использования. поставщиков, чтобы иметь две версии всех основных библиотек поддержки. Даже что-то вроде MBED, которое представляет собой C++ API и среду, по-прежнему использует код C поставщика чипов для специфики большинства своих целевых уникальных серверных частей.
@ChrisStratton Есть ли что-то конкретное, что я написал, что является неточным и требует изменения?

Можно ли совместить C и C++ в одном проекте?

Да, но требуется достаточный уход. Если у вас есть библиотека, написанная на C, то при условии, что вы поместите вокруг нее оболочку C++ и знаете, что она там, все должно быть в порядке.

Хотя это возможно , общее правило состоит в том, чтобы избегать смешанной разработки там, где это возможно .

Большая проблема заключается в том, что компиляторы должны быть совместимы; объявление функции C++, имеющей связь с C, означает, что функция C++ должна возвращать тип, пригодный для использования в C.

Использование:

В среде C++

Объявление функции C

extern "C" Cfunc(int); // that is one way

Есть и другие - некоторые ссылки

Общие причины избегать смешивания C и C++ сводятся к удобству сопровождения (в конце концов, в настоящее время это два очень разных языка) и тому факту, что подмножество C в C++ не соответствует стандарту ANSI C, поэтому некоторые вещи могут работать не так, как хотелось бы .

Можете ли вы объяснить, почему лучше избегать смешивания c и c++? Я имею в виду, что сегодня keil iar eclipse и другие интегрированные среды разработки, встроенные в c, хорошо обновлены. Есть ли еще одна причина избегать этой практики