Объединение глобальных массивов во время соединения/заполнение глобального массива из нескольких единиц компиляции

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

Вот пример того, что я хотел бы сделать (но не работает).

central.h:

typedef void (*callback_t)(void);

callback_t callbacks[];

central.c:

#include "central.h"

void do_callbacks(void) {
    int i;
    for (i = 0; i < sizeof(callbacks) / sizeof(*callbacks); ++i)
        callbacks[i]();
}

foo.c:

#include "central.h"

void callback_foo(void) { }

callback_t callbacks[] = {
    &callback_foo
};

bar.c:

#include "central.h"

void callback_bar(void) { }

callback_t callbacks[] = {
    &callback_bar
};

Что бы я хотел, это получить один массив callbacks, который содержит два элемента: &callback_foo и &callback_bar. С помощью приведенного выше кода есть очевидно, две проблемы:

  • Массив callbacks определяется несколько раз.
  • sizeof(callbacks) не известен при компиляции central.c.

Мне кажется, что первый момент может быть решен путем объединения линкера два символа callbacks вместо того, чтобы бросать ошибку (возможно, через некоторые атрибут для переменной), но я не уверен, есть ли что-то подобное. Даже если есть, проблема sizeof также должна быть решена.

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

  • Используйте динамическую память (realloc) для массива обратных вызовов.
  • Используйте статическую память с фиксированным размером (как обычно, требуемым).

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

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

Это решение - лучшее, что я нашел до сих пор. Он использует связанный список который заполняется во время выполнения, но использует память, распределенную статически в каждом (например, следующий указатель выделяется с каждым указатель функции). Тем не менее, накладные расходы этих следующих указателей не должны требуется - есть ли лучший подход?

Возможно, наличие динамического решения в сочетании с оптимизацией времени соединения может как-то приводят к статическому распределению?

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

Далее

  • Использование С++ в порядке, я просто использовал код C выше для иллюстрации проблемы, в большинстве случаев код Arduino - это С++.
  • Я использую gcc/avr-gcc, и хотя я бы предпочел переносное решение, то только gcc только нормально.
  • У меня есть поддержка шаблонов, но не STL.
  • В среде Arduino, которую я использую, у меня нет Makefile или другого способа легко запустить какой-то пользовательский код в compiletime, поэтому я ищу что-то, что может быть полностью реализовано в коде.

Ответ 1

Как отмечалось в предыдущем ответе, лучшим вариантом является использование пользовательского компоновщика script (с разделом ввода KEEP(*(SORT(.whatever.*)))).

В любом случае это можно сделать без изменения сценариев компоновщика (рабочий пример кода ниже), по крайней мере на некоторых платформах с gcc (протестирован на встроенном устройстве xtensa и cygwin)

Предположения:

  • Мы хотим избежать использования ОЗУ как можно больше (встроенного)
  • Мы не хотим, чтобы вызывающий модуль знал что-либо о модулях с обратными вызовами (это lib)
  • Нет фиксированного размера для списка (неизвестный размер во время компиляции библиотеки)
  • Я использую GCC. Принцип работы может работать и на других компиляторах, но я его не тестировал.
  • Функции обратного вызова в этом примере не принимают никаких аргументов, но при необходимости их легко модифицировать

Как это сделать:

  • Нам нужен компоновщик, чтобы как-то выделить во время ссылки массив указателей на функции
  • Поскольку мы не знаем размер массива, нам также нужен компоновщик, чтобы как-то пометить конец массива

Это довольно специфично, так как правильный способ - использовать пользовательский компоновщик script, но это возможно, если это не произойдет, если мы найдем раздел в стандартном компоновщике script, который всегда "хранится" и "отсортирован".

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

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

Кроме того, когда мы помещаем указатели в раздел конструкторов, нам нужно использовать один байт ОЗУ (для всей программы), чтобы пропустить код обратного вызова во время выполнения C init.


test.c:

Библиотека, которая регистрирует модуль с именем test и вызывает его обратные вызовы в некоторой точке

#include "callback.h"

CALLBACK_LIST(test);

void do_something_and_call_the_callbacks(void) {

        // ... doing something here ...

        CALLBACKS(test);

        // ... doing something else ...
}

callme1.c:

Клиентский код, регистрирующий два обратных вызова для модуля test. Сгенерированные функции не имеют имени (действительно, у них есть имя, но оно волшебным образом генерируется как уникальное внутри единицы компиляции)

#include <stdio.h>
#include "callback.h"

CALLBACK(test) {
        printf("%s: %s\n", __FILE__, __FUNCTION__);
}

CALLBACK(test) {
        printf("%s: %s\n", __FILE__, __FUNCTION__);
}

void callme1(void) {} // stub to be called in the test sample to include the compilation unit. Not needed in real code...

callme2.c:

Клиентский код, регистрирующий другой обратный вызов для модуля test...

#include <stdio.h>
#include "callback.h"

CALLBACK(test) {
        printf("%s: %s\n", __FILE__, __FUNCTION__);
}

void callme2(void) {} // stub to be called in the test sample to include the compilation unit. Not needed in real code...

callback.h:

И волшебство...

#ifndef __CALLBACK_H__
#define __CALLBACK_H__

#ifdef __cplusplus
extern "C" {
#endif

typedef void (* callback)(void);
int __attribute__((weak)) _callback_ctor_stub = 0;

#ifdef __cplusplus
}
#endif

#define _PASTE(a, b)    a ## b
#define PASTE(a, b)     _PASTE(a, b)

#define CALLBACK(module) \
        static inline void PASTE(_ ## module ## _callback_, __LINE__)(void); \
        static void PASTE(_ ## module ## _callback_ctor_, __LINE__)(void); \
        static __attribute__((section(".ctors.callback." #module "$2"))) __attribute__((used)) const callback PASTE(__ ## module ## _callback_, __LINE__) = PASTE(_ ## module ## _callback_ctor_, __LINE__); \
        static void PASTE(_ ## module ## _callback_ctor_, __LINE__)(void) { \
                 if(_callback_ctor_stub) PASTE(_ ## module ## _callback_, __LINE__)(); \
        } \
        inline void PASTE(_ ## module ## _callback_, __LINE__)(void)

#define CALLBACK_LIST(module) \
        static __attribute__((section(".ctors.callback." #module "$1"))) const callback _ ## module ## _callbacks_start[0] = {}; \
        static __attribute__((section(".ctors.callback." #module "$3"))) const callback _ ## module ## _callbacks_end[0] = {}

#define CALLBACKS(module) do { \
        const callback *cb; \
        _callback_ctor_stub = 1; \
        for(cb =  _ ## module ## _callbacks_start ; cb <  _ ## module ## _callbacks_end ; cb++) (*cb)(); \
} while(0)

#endif

main.c:

Если вы хотите попробовать... это точка входа для отдельной программы (протестирована и работает на gcc-cygwin)

void do_something_and_call_the_callbacks(void);

int main() {
    do_something_and_call_the_callbacks();
}

выход:

Это (актуальный) вывод в моем встроенном устройстве. Имена функций генерируются в callback.h и могут иметь дубликаты, поскольку функции являются статическими

app/callme1.c: _test_callback_8
app/callme1.c: _test_callback_4
app/callme2.c: _test_callback_4

И в CygWIN...

$ gcc -c -o callme1.o callme1.c
$ gcc -c -o callme2.o callme2.c
$ gcc -c -o test.o test.c
$ gcc -c -o main.o main.c
$ gcc -o testme test.o callme1.o callme2.o main.o
$ ./testme
callme1.c: _test_callback_4
callme1.c: _test_callback_8
callme2.c: _test_callback_4

карта компоновщика:

Это соответствующая часть файла карты, сгенерированного компоновщиком

 *(SORT(.ctors.*))
 .ctors.callback.test$1    0x4024f040    0x0    .build/testme.a(test.o)
 .ctors.callback.test$2    0x4024f040    0x8    .build/testme.a(callme1.o)
 .ctors.callback.test$2    0x4024f048    0x4    .build/testme.a(callme2.o)
 .ctors.callback.test$3    0x4024f04c    0x0    .build/testme.a(test.o)

Ответ 2

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

Однако вы сделали глобальную переменную в файле заголовка, доступной каждому модулю, включая этот заголовок. Это вводит плотную связь между всеми такими файлами, хотя они не связаны друг с другом. Кроме того, кажется, что функция call-обработчика callback должна фактически вызывать функции, но они подвержены всей программе.

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

И на самом деле нет очевидных причин, по которым вам нужно выделить этот массив во время компиляции. Единственной разумной причиной было бы сохранение ОЗУ, но это, конечно, является веской причиной для встроенной системы. В этом случае массив должен быть объявлен как const и инициализирован во время компиляции.

Вы можете сохранить что-то похожее на ваш дизайн, если хранить массив как объекты чтения-записи. Или, если массив должен быть только для чтения с целью сохранения ОЗУ, вы должны сделать резкий пересмотр.

Я дам обе версии, подумайте, какой из них наиболее подходит для вашего случая:

Массив чтения/записи на основе ОЗУ

(Преимущество: гибкое, может быть изменено во время выполнения. Недостатки: потребление ОЗУ. Незначительный над головой код для инициализации. RAM больше подвержена ошибкам, чем вспышка.)

  • Позвольте callback.h и callback.c из модуля, который связан только с обработкой функций обратного вызова. Этот модуль отвечает за то, как распределяются обратные вызовы и когда они выполняются.
  • В callback.h определяется тип функций обратного вызова. Это должен быть тип указателя функции, как и вы. Но удалите объявление переменной из файла .h.
  • В callback.c объявить массив обратных вызовов функций

     static callback_t callbacks [LARGE_ENOUGH_FOR_WORST_CASE];
    
  • Невозможно избежать "LARGE_ENOUGH_FOR_WORST_CASE". Вы находитесь на встроенной системе с ограниченной оперативной памятью, поэтому вам нужно действительно рассмотреть, что такое наихудший сценарий, и зарезервировать для этого достаточно памяти, не более того, не меньше. В встроенной системе микроконтроллера нет таких вещей, как "обычно необходимо" и "позволяет сохранить некоторую ОЗУ для других процессов". В вашем MCU есть достаточно памяти для покрытия наихудшего сценария, или нет, и в этом случае количество умных ассигнований не спасет вас.

  • В callback.c объявите переменную размера, которая отслеживает, сколько из массива обратного вызова, который был инициализирован. static size_t callback_size;.

  • Напишите функцию init void callback_init(void), которая инициализирует модуль обратного вызова. Прототип должен быть в файле .h, и вызывающий отвечает за его выполнение один раз при запуске программы.
  • Внутри функции init установите callback_size в 0. Причина, по которой я предлагаю сделать это во время выполнения, заключается в том, что у вас есть встроенная система, где сегмент .bss может отсутствовать или даже нежелателен. Возможно, у вас даже нет кода копирования, который инициализирует все статические переменные до нуля. Такое поведение несоответствует стандарту C, но очень распространено во встроенных системах. Поэтому никогда не записывайте код, который полагается на статические переменные, автоматически инициализируемые до нуля.
  • Напишите функцию void callback_add (callback_t* callback);. Каждый модуль, который включает ваш модуль обратного вызова, вызывается этой функцией, чтобы добавить в список свои функции обратного вызова.
  • Сохраняйте свою функцию do_callbacks так, как она есть (хотя, как второстепенное замечание, рассмотрите переименование на callback_traverse, callback_run или подобное).

Массив для чтения на основе Flash

(Преимущества: экономия памяти RAM, истинное постоянное запоминающее устройство, безопасное из-за ошибок памяти. Недостатки: менее гибкие, зависит от каждого модуля, используемого в проекте, возможно, немного медленнее, поскольку он во вспышке.)

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

Вместо того, чтобы иметь несколько несвязанных модулей, включая модуль обработчика обратного вызова, вам нужно сделать модуль обработчика обратного вызова включением всего остального. Отдельные модули по-прежнему не знают, когда обратный вызов будет выполнен или где он будет выделен. Они просто объявляют одну или несколько функций как обратные вызовы. Затем модуль обратного вызова отвечает за добавление каждой такой функции обратного вызова в свой массив во время компиляции.

// callback.c

#include "timer_module.h"
#include "spi_module.h"
...

static const callback_t CALLBACKS [] = 
{
  &timer_callback1,
  &timer_callback2,
  &spi_callback,
  ...
};

Преимущество этого заключается в том, что вы автоматически получите сценарий наихудшего случая, переданный вам по вашей собственной программе. Размер массива теперь известен во время компиляции, это просто sizeof(CALLBACKS)/sizeof(callback_t).

Конечно, это не так элегантно, как общий модуль обратного вызова. Вы получаете плотную связь от модуля обратного вызова ко всем другим модулям проекта, но не наоборот. По сути, callback.c является "main()".

Вы все равно можете использовать указатель функции typedef в callback.h, но он больше не нужен: отдельные модули должны гарантировать, что у них есть свои функции обратного вызова, записанные в желаемом формате, так или иначе с таким типом присутствия.

Ответ 3

Я тоже столкнулся с аналогичной проблемой:

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

Mine is C, на процессоре Atmel XMega. Вы упомянули, что используете GCC. Следующее не решает вашу проблему, это вариант для решения выше # 1. Он использует директиву __attribute__((weak)).

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

fooModule.c:
void foo_eventCallback(void) {
    // do the foo response here
}

barModule.c:
void bar_eventCallback(void) {
    // do the bar response here
}

yakModule.c:
void yak_eventCallback(void) {
    // do the yak response here
}

2) Укажите начальную точку обратного вызова, которая выглядит примерно так:

__attribute__((weak)) void foo_eventCallback(void) { }
__attribute__((weak)) void bar_eventCallback(void) { }
__attribute__((weak)) void yak_eventCallback(void) { }

void functionThatExcitesCallback(void) {
    foo_eventCallback();
    foo_eventCallback();
    foo_eventCallback();
}

Классификатор __attribute__((weak)) в основном создает реализацию по умолчанию с пустым телом, которое компоновщик заменит другим вариантом, если он найдет не-слабый вариант с тем же именем. К сожалению, это не делает его полностью развязанным. Но вы можете, по крайней мере, поместить этот большой супер-набор всех обратных вызовов в одно и только одно место, а не вдаваться в заголовок с адским файлом. И тогда ваши разные единицы компиляции в основном заменяют подмножества надмножества, которые они хотят. Мне бы это понравилось, если бы был способ сделать это с использованием одной и той же именованной функции во всех модулях и просто вызвать те, которые были вызваны на основе того, что связано, но еще не нашли того, что это делает.