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

При создании библиотек, для использования в нескольких проекта, часто возникает проблема использования в них констант (например, номеров пинов) специфичных для конкретного проекта. Хотелось бы, чтобы константы, определённые в проекте, были доступны и библиотеке. Например, можно написать вот так:

#define	THE_PIN	13
#include <mumu.h>

теперь константа THE_PIN доступна при компиляции файла mumu.h.

Однако, всё сложнее, если в библиотеке, кроме включаемых по #include файлов, есть ещё и самостоятельные файлы (например, в данном случае, кроме mumu.h есть ещё и mumu.cpp). Сделать наши константы доступными в таких отдельных (не включаемых явно) файлах нет никакой возможности.

зануды, идите лесом

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

Ну, казалось бы, какие проблемы - пиши всё, что нужно во включаемом файле и не парься. Правда, некоторые “яйцеголовые” говорят, что во включаемых файлах нельзя определять глобальные переменные, и глобальные не-inline функции, но наш первый же эксперимент показывает, что это чушь! Например, вот такая замечательная библиотека прекрасно работает:

Файл kaka.ino
#define	THE_PIN	11
#include "mumu.h"

void setup(void) {
	Serial.begin(9600);
	mumu.bark();
	simplePrint("from kaka.ino");
}

void loop(void) {
	delay(100500);
}
Библиотечный файл 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 mumu(THE_PIN);

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

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

#endif //	MUMU_H

Запускаем, проверяем, радуемся! И глобальный экземпляр объявлен, и глобальная не-inline функция, и всё работает! Жизнь-то налаживается!

Однако, как следует из закона Мэрфи, «если Вам кажется, что ситуация улучшается, значит Вы чего-то не заметили».

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

Файл kaka.ino
#include "pinout.h"
#include "mumu.h"

extern void kukuBark(void);

void setup(void) {
	Serial.begin(9600);
	mumu.bark();
	simplePrint("from kaka.ino");
	kukuBark();
}

void loop(void) {
	delay(100500);
}
Файл pinout.h
#ifndef	PINOUT_H
#define	PINOUT_H

#define	THE_PIN	11

#endif	//		PINOUT_H
Файл kuku.cpp
#include <Arduino.h>
#include "pinout.h"
#include "mumu.h"

void kukuBark(void) {
	Serial.print("From kuku.cpp file - ");
	mumu.bark();
	simplePrint("from kuku.cpp");
}
Библиотечный файл 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 mumu(THE_PIN);

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

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

#endif //	MUMU_H

Запускаем сборку и … огребаем :frowning:

Ругань сборщика
sketch\kuku.cpp.o (symbol from plugin): In function `CMumu::bark()':
(.text+0x0): multiple definition of `CMumu::bark()'
sketch\kaka.ino.cpp.o (symbol from plugin):(.text+0x0): first defined here
sketch\kuku.cpp.o (symbol from plugin): In function `CMumu::bark()':
(.text+0x0): multiple definition of `simplePrint(char const*)'
sketch\kaka.ino.cpp.o (symbol from plugin):(.text+0x0): first defined here
sketch\kuku.cpp.o (symbol from plugin): In function `CMumu::bark()':
(.text+0x0): multiple definition of `mumu'
sketch\kaka.ino.cpp.o (symbol from plugin):(.text+0x0): first defined here
collect2.exe: error: ld returned 1 exit status
exit status 1
Ошибка компиляции для платы Arduino Uno.

А ведь так всё хорошо было!

Чтобы разобраться, что произошло, надо чётко понимать смысл директивы #include. А смысл этот таков: директива #include абсолютно равнозначна тому, что Вы просто “вкопипастили” указанный в ней файл в то место своей программы, где она расположена. Ничего другого она не делает, просто тупо вставляет в это место файл, который в ней указан.

Теперь становится понятным на что ругается сборщик. Из-за того, что мы “вкопипастили” файл mumu.h и в файл kaka.ino, и в файл kuku.cpp, получилось, что функции CMumu::bark(), simplePrint(char const*), а также переменная mumu в нашей программе определены дважды - один раз в kaka.ino, и второй раз в kuku.cpp. Вот об этом нам вежливо и сообщили.

Ну, кто виноват разобрались (китайцы, пиндосы, евреи, кто там ещё …). Остался вопрос: что же делать?

Выход есть!

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

Таким образом, если мы добавим атрибут weak в определения наших функций CMumu::bark(), simplePrint(char const*) и перемнной mumu, то проблема должна решиться. Пробуем:

Файл kaka.ino
#include "pinout.h"
#include "mumu.h"

extern void kukuBark(void);

void setup(void) {
	Serial.begin(9600);
	mumu.bark();
	simplePrint("from kaka.ino");
	kukuBark();
}

void loop(void) {
	delay(100500);
}
Файл pinout.h
#ifndef	PINOUT_H
#define	PINOUT_H

#define	THE_PIN	11

#endif	//		PINOUT_H
Файл kuku.cpp
#include <Arduino.h>
#include "pinout.h"
#include "mumu.h"

void kukuBark(void) {
	Serial.print("From kuku.cpp file - ");
	mumu.bark();
	simplePrint("from kuku.cpp");
}
Библиотечный файл 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);

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

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

#endif //	MUMU_H

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

Ну, вот, как-то так. Можно пользоваться.

В код попадёт только один экземляр каждого объекта, так что никакого увеличения кода не будет. Единственное, чем придётся заплатить - небольшим увеличением времени компиляции, но настолько небольшим, что, если специально не будете замерять, то не заметите.

Не забываем, что это атрибут GCC, и в стандарте языка его нет. Поэтому, если программа должна компилироваться ещё каким-то компилятором, необходимо убедиться, что такой атрибут там есть и работает он там также.

7 лайков

Спасибо
Не знал что weak можно к переменным применять

Правильно ли я понял, что в GCC, идущим в комплекте с ArduinoIDE, нет поддержки C++17?

определение в заголовке без inline

И? Не понял замечания (или вопроса).

Ардуино - оно большое, в него входит порядка полусотни пакетов для разных архитектур. И версии и конфиг компилятора отличаются от пакета к пакету.

С пакетом от MiniCore есть поддержка c++17
Компиляция скетча... "D:\\SoftWare\\arduino-1.8.19\\portable\\packages\\arduino\\tools\\avr-gcc\\7.3.0-atmel3.6.1-arduino7/bin/avr-g++" -c -g -Os -Wall -Wextra -std=gnu++17 -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -MMD -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10819 -DARDUINO_AVR_ATmega328 -DARDUINO_ARCH_AVR "-ID:\\SoftWare\\arduino-1.8.19\\portable\\packages\\MiniCore\\hardware\\avr\\2.2.2\\cores\\MCUdude_corefiles" "-ID:\\SoftWare\\arduino-1.8.19\\portable\\packages\\MiniCore\\hardware\\avr\\2.2.2\\variants\\standard" "C:\\Windows\\TEMP\\arduino_build_931361\\sketch\\sketch_jun01a.ino.cpp" -o "C:\\Windows\\TEMP\\arduino_build_931361\\sketch\\sketch_jun01a.ino.cpp.o"
В platform.txt можно поменять опции компиляции !

Ого, даже у каждого пакета “свой” компилятор? Интересно, этого не знал.
Просто удивлен “нетрадиционному” использованию weak вместо удобного для C++17 inline.

Для неискушенных читателей я поясню суть обмена “любезностями” между Ркит и ЕвгенийП.
MISRA rule 8.5 запрещает в хедере размещать код, кроме static или inline.

Со своей стороны должен сказать, что 90% разработчиков срать хотели на MISRA и совершенно правильно. :joy: Если ваш работодатель требует соответствия MISRA, то с вашей стороны нужно требовать удвоения ЗП и молоко за вредность.

Ну и “вишенкой” следует добавить, что МИСРА также не приветствует GCC экстеншены к стандарту, так что аттрибут weak с их точки зрения - харам! :cowboy_hat_face:

Спасибо за информацию!

Кстати, почитав немного в интернете про этот “хак” с линковкой, нашел, что компилятор clang, видя атрибут weak, игнорирует квалификатор constexpr у переменной, что приводит к невозможности использования такой переменной в compile-time.
Так что будьте осторожны, с учетом многообразия ардуин =)

Для avr компилятор один 7.3.0

даже более - у одного пакета может быть несколько версий компилятора. Хотя это обычно гемор и так не делают.

Но то что у разных архитектур будут разные компилеры - что в этом странного

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

эта библиотека работает с SPI, пины для которого на большинстве ардуин фиксированы, зачем их править?

иногда надо на нестандартных пинах, у меня скетчи в последнее время мультиконтроллерные, ESP32 (разные) и RP2040 и хочется чтобы всё компилировалось от простого выбора нужного контроллера

Хочешь сказать, что либа не принимает пины как параметры?
Ну тогда значит конфиг править. Только не в библиотеке, а в папке скетча - посмотри примеры.

Только это такой монстр (я о этой библиотеке) - смотри не утони :slight_smile:

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

Неправильно.

Даже 20-ка частично поддерживается. Только опции надо правильно указать. По умолчанию сказано поддерживать 11.

7.3.0 вроде от 18 года ?

Вы просто невнимательно читали пост. Он специально посвящён не-inline вещам. Потому, что с inline этой проблемы нет.

Там ведь прямо написано:

Т.е. пост про не-inline.

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

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