«Слабый» трюк с библиотеками

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

Тогда объясните мне доходчивей, пожалуйста, к чему был посвящен ваш пост.

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

Хотелось бы, чтобы константы, определённые в проекте, были доступны и библиотеке.
Однако, всё сложнее, если в библиотеке, кроме включаемых по #include файлов, есть ещё и самостоятельные файлы

Ну, казалось бы, какие проблемы - пиши всё, что нужно во включаемом файле и не парься.

Проблема №1: Хотим чтобы библиотека воспринимала наши константы (на самом деле макросы).
Решение: Переписать библиотеку под single-file и перед включением в компилируемый файл обязательно подключать наш набор макросов, которыми мы хотели поделиться с библиотекой.
Примечание №1: В среде ArduinoIDE отсутствует возможность типа ‘global preinclude’ (встраивание выбранных файлов ко всем ‘translation unit’ в самом начале)? Тогда можно оставить библиотеку в изначальном виде, а файлик pinout.h подключить всюду.
Примечание №2: Допустим, не хотим светить нашим набором макросов во все единицы трансляции. Тогда почему бы просто не завести еще один файлик с конфигурацией библиотеки, куда и будем записывать нужные нам макросы

mumu.config.h
#ifndef MUMU_CONFIG_H
#define MUMU_CONFIG_H

#include "pinout.h"

#endif //	MUMU_CONFIG_H
mumu.h
#ifndef MUMU_H
#define MUMU_H

#if __has_include("mumu.config.h")
#  include "mumu.config.h"
#else
#  error "Configuration file 'mumu.config.h' does not exist! Create it in your project directory!"
#endif

//------------------------------------------------------------------------------
// Дальше полная копия оригинального файла, со всеми остановками
//------------------------------------------------------------------------------

#endif //	MUMU_H

Включение файлов будет выглядеть следующим образом:
pinout.h >>> mumu.config.h >>> mumu.h >>> mumu.cpp
В изначальном варианте все равно предлагалось “переписать” библиотеку (объединить два файла *.c и *.h в один *.h файл), в чем принципиальная разница в этих двух вариантах переделки?

Всё прекрасно лишь до тех пор, пока в нашем проекте есть только один файл, в который включается наша библиотека.

В GCC есть замечательный атрибут weak . Означает он следующее: … а если все были с этим атрибутом, то использую первый, который встретился.

Проблема №2: При включении в include-файл определение функций и переменных без дополнительных квалификаторов, у нас появляются копии этих функций и переменных в каждом ‘translation unit’ из-за тупой копипасты директивы include.
Решение: Добавим ко всем проблемным функциям и переменным атрибут weak, основное назначение которого - возможность переопределения “слабой” функции/переменной другой “сильной” функцией/переменной из любой другой единицы трансляции. При этом эта основная особенность не используется (в файле kaka.ino нет переопределения функции simplePrint и переменной mumu), а используется побочная особенность - допустимость множественного определения, при создании кода “слабой” функции/переменной только в одном объектном файле.
Примечание: Как я понял, применение атрибута weak обусловлено именно его побочными особенностями:

Запускаем и убеждаемся, что теперь всё работает и с включением библиотеки в два файла проекта (и во сколько угодно).
В код попадёт только один экземляр каждого объекта, так что никакого увеличения кода не будет.

Однако ровно то же самое выполняет квалификатор inline:

An inline function or inline variable (since C++17) has the following properties:

2. An inline function or variable (since C++17) with external linkage (e.g. not declared static) has the following additional properties:

  1. There may be more than one definition of an inline function or variable (since C++17) in the program as long as each definition appears in a different translation unit and (for non-static inline functions and variables (since C++17)) all definitions are identical. For example, an inline function or an inline variable (since C++17) may be defined in a header file that is included in multiple source files.
  2. It has the same address in every translation unit.

Я не работал с GCC, но очень сомневаюсь, что квалификатор inline в нем используется также, как и в C89:

The original intent of the inline keyword was to serve as an indicator to the optimizer that inline substitution of a function is preferred over function call, that is, instead of executing the function call CPU instruction to transfer control to the function body, a copy of the function body is executed without generating the call.

Since this meaning of the keyword inline is non-binding, compilers are free to use inline substitution for any function that’s not marked inline, and are free to generate function calls to any function marked inline. Those optimization choices do not change the rules regarding multiple definitions and shared statics listed above.
Because the meaning of the keyword inline for functions came to mean “multiple definitions are permitted” rather than “inlining is preferred”, that meaning was extended to variables (since C++17).

P.S. Я действительно плохо знаком с GCC и ArduinoIDE, поэтому могу не знать каких-либо тонкостей.

Что касается inline - это не везде применимо. Если функция большая и используется в нескольких местах, то inline - дорого.

Дорого - в денежном эквиваленте?
В контексте длительности компиляции - возможно, но сомнительно:

  1. С атрибутом weak компилятор (скорее даже линковщик?) ищет “сильное” определение объекта в других единицах трансляции, а этих единиц может быть много;
  2. С квалификатором inline компилятор (или опять же линковщик?) ищет в ранее созданных *.o - файлах определение “заинлайненного” объекта, если находит - то в текущей единице трансляции дает на его ссылку, если не находит - то создает определение в текущей единице трансляции.
    Плюс-минус то на то.

В контексте занимаемого исходного кода - одно и то же. Проверял в ICCARM, но возможно у GCC свои заскоки (может, позже проверю в облачном компиляторе, т.к. физического нема).

А с inline -переменными (в 17 не помню, а в 20 - точно есть) и вообще … если переменная реально нужна глобальная, то чем Вам поможет inline ?

inline constexpr в заголовочном файле.
А вообще речь шла про объект mumu класса CMumu, который определен в заголовке, который подключается к разным единицам трансляции, а дублировать его не надо.

Где? В директории библиотеки? Тогда он не будет “своим” для каждого проекта. А если в директории проекта, то … приведите пожалуйста, работающий в IDE пример, тогда обсудим насколько это удобно и здорово.

Уверяю Вас, когда Вы начнёте писать пример, у Вас поубавится энтузиазма, связанно с таким вариантом. У меня как раз этот случай описывался в ремарке “Зануды идите лесом”.

Вот-вот. С этого места, пожалуйста, работающий в IDE примерчик!

Нет!

Речь шла о написании новых библиотек.

Это работает в IDE “из коробки”? Я думаю, что - нет. Если Вы думаете иначе, опять же, пожалуйста, работающий пример.

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

так и не понял, как вытащить файл User_Setup.h из библиотеки TFT_eSPI в директорию проекта

А зачем его вытаскивать? Все файлы проекта должны жить в директории проекта. Нельзя хранить фал проекта в директории библиотеки, иначе он будет изменён при разработке нового проекта.

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

Возможно, @Azeront знает неговнокодный вариант и покажет нам.

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

А если в директории проекта, то … приведите пожалуйста, работающий в IDE пример, тогда обсудим насколько это удобно и здорово.

Правильно ли я понял вас, в ArduinoIDE нельзя определять собственные дополнительные каталоги для директивы include (т.н. include directories)? При этом сама IDE автоматически не добавляет каталог активного проекта в include directories? Если все так, тогда вы правы, добавить файл конфигурации библиотеки в каталог проекта проблематично.

Попутно возник вопрос - а как тогда подключаются заголовочники библиотек? Если библиотека в каталоге проекта - то через relative path в директиве include (как я понял). А если она скачана через менеджер библиотек? Для такого варианта ArduinoIDE сама прописывает include directories?

Это работает в IDE “из коробки”?

А какой тулчейн у ArduinoIDE “из коробки”? Или их несколько (т.к. в исходной поставке несколько пакетов)? Исходя из ранее написанных сообщений в этой теме, под “коробкой” имеется ввиду avr-gcc 7.3.0 без опции “-std=gnu++17”?

На сайте ардуино нашел Arduino Web Editor. Внес ваш оригинальный пример, в качестве платы выбрал Arduino Nano (вроде как одна из самых популярных), скомпилировал:

kaka - результат компиляции

/usr/local/bin/arduino-cli compile --fqbn arduino:avr:nano:cpu=atmega328 --libraries /home/builder/opt/libraries/latest --build-cache-path /tmp --output-dir /tmp/106892210/build --build-path /tmp/arduino-build-60E772CAB4FC84BADD4B4491A6EACC66 /tmp/106892210/kaka

Sketch uses 2108 bytes (6%) of program storage space. Maximum is 30720 bytes.

Global variables use 271 bytes (13%) of dynamic memory, leaving 1777 bytes for local variables. Maximum is 2048 bytes.

Затем в вашем же примере, в файле mumu.h заменил все __attribute__((weak)) на inline, полученный результат также откомпилировал:

kaka2 - результат компиляции

/usr/local/bin/arduino-cli compile --fqbn arduino:avr:nano:cpu=atmega328 --libraries /home/builder/opt/libraries/latest --build-cache-path /tmp --output-dir /tmp/351649452/build --build-path /tmp/arduino-build-73AED8E3A6A4A6A6337A9383F65944F7 /tmp/351649452/kaka2

Sketch uses 2108 bytes (6%) of program storage space. Maximum is 30720 bytes.

Global variables use 271 bytes (13%) of dynamic memory, leaving 1777 bytes for local variables. Maximum is 2048 bytes.

По итогу - тот же самый размер бинарника. Или так не считается, поскольку это другая “коробка”? Тогда прошу прощения, с установкой standalone-версии программы заморачиваться не стал.

… более продуктивно было бы не наезжать на приведённый пост …

Процитируйте наезд(ы), пожалуйста.

… описать свой подход …

В моем понимании - для решения проблемы библиотеки, выполненной в виде одного *.h-файла (при ее подключении в разные единицы трансляции), необходимо все функции и глобальные переменные помечать квалификатором inline, поскольку он именно для этого и используется. Использование же weak оставляет возможность “выстрелить себе в ногу”, переопределив библиотечную функцию/переменную (из контекста первоначального поста, как я понял, такая функциональность библиотеке из примера не нужна).
Единственная причина, почему нельзя пользоваться inline (в моем понимании) - это не полная его реализация до C++17 (до этого поддерживались только функции, но не переменные). И именно такой случай в вашем примере в файле mumu.h, поэтому мой первый вопрос в теме и был про C++17.

Мои посты - это не “наезды”, а попытка понять причину не использования стандартной конструкции языка. Судя по текущему диалогу, дело не только в отсутствии “из-коробочной” поддержки C++17, но и в других ограничениях ArduinoIDE, о которых я не в курсе.

Не очень вас понял. Если вы про то, что квалификатор inline не применим к переменным в “ванильной” ArduinoIDE (без доп. настроек и ухищрений) - то это вроде логично, если нет поддержки C++17 (об этом я и спрашивал изначально).
Если вы про то, что при замене у методов и функций атрибута __attribute__((weak)) на квалификатор inline скетч перестанет собираться, или бинарный результат сборки будет отличаться от оригинального результата - то это не так, и это гарантировано стандартом языка C++. Если же в ArduinoIDE есть какие-то хитрые “бзики”, о которых не знаю я, но ведаете вы, и именно они опровергают все мои выводы - расскажите же о них, не томите. Конкретно для этого варианта пример простой:

Библиотечный файл mumu.h
#ifndef MUMU_H
#define MUMU_H

#ifndef THE_PIN
	#error THE_PIN must be defined before "#include <mumu.h>" statement
#endif

//
//	Суперполезный класс
//
class CMumu {
	uint8_t m_pin;
public:	
	CMumu(const uint8_t pin) : m_pin(pin) {}
	void bark(void);
};

//
// Объявляем общий для всех, глобавльный экземпляр
// ( типа как объявлен Serial )
//
CMumu __attribute__((weak)) mumu(THE_PIN);

//
//	Метод класса, слишком большой чтобы определять прямо в классе
//
inline void CMumu::bark(void) {
	Serial.print("Pin is: ");
	Serial.println(m_pin);
}

//
// Просто глобальная функция (не член класса)
//
inline void simplePrint(const char *s) {
	Serial.print("Simple Print: ");
	Serial.println(s);
}

#endif //	MUMU_H

@Azeront,

т.е. работающего примера в IDE из коробки не будет?

Ну, тогда на этом и остановимся.