При создании библиотек, для использования в нескольких проекта, часто возникает проблема использования в них констант (например, номеров пинов) специфичных для конкретного проекта. Хотелось бы, чтобы константы, определённые в проекте, были доступны и библиотеке. Например, можно написать вот так:
#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
Запускаем сборку и … огребаем
Ругань сборщика
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, и в стандарте языка его нет. Поэтому, если программа должна компилироваться ещё каким-то компилятором, необходимо убедиться, что такой атрибут там есть и работает он там также.