Вопросы архитектуры кода, или Low Coupling на C. Делегируем обязанности.

Привет-привет, дорогие друзья.

Программирую я на Си не так уж и много, но есть определённый опыт работы на Objective-C, и в парадигме ООП одним из основополагающих принципов является Low Coupling — низкая связанность объектов, которая позволяет максимально легко переиспользовать написанные классы, тем самым увеличивая эффективность разработки в разы.

Естественно, говоря о Си, мы не можем говорить об объектах, но хотелось бы получить слабую связанность модулей, чтобы можно было их легко добавлять в разные проекты, по минимуму задумываясь о том количестве хвостов, что тащит за собой модуль. Один из способов — использовать функции с атрибутами weak alias в GCC. Правильно ли это или нет — я не знаю, возможно есть и решения по-красивее, но цели, которые ставились более-менее достигнуты. Скорее всего речь пойдёт об абсолютно тривиальных вещах, но я реально не знал, как это делается в Си, по-этому Всех кого преамбула заинтриговала, или, может быть, наоборот, заставила негодовать — добро пожаловать под кат)

Delegate Em'All!


Итак, чего хотелось?

Что такое Delegate и зачем его использовать.

В Objective-C один из весьма популярных и используемых паттернов — Delegate. Суть его в том, чтобы объект, на зная как исполнить какой-либо из методов, просто отдавал этот метод на исполнение своему делегату. Один из простейших примеров — мы щелкаем по полю ввода, которое является стандартным компонентом, но в конкретном приложении в поле ввода нам нужны только, допустим, цифры и буквы ABCDEF — ну вы поняли) Так как поле — компонент стандартный, оно знает как вводить любые цифры и буквы, но не знает, что конкретно нам нужно. Для него мы назначаем делегата — объект, который имплементирует нашу бизнес-логику — а именно, проверяет при вводе символа, валидность этого самого символа и возвращает BOOL — можно показывать этот символ, или проигнорировать. То есть поле получает символ, но перед тем как показать его спрашивает у делегата — Делегат, а можно показать этот символ? Делегат же, заключая в себе весьма простую логику работы, просто разрешает полю показывать символы или не разрешает.

Delegate All The Things!

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

Common work with a delegate

Чем это отличается просто от вызова функции из подключаемой библиотеки? Тем, что прототип функции, которую вызывает компонент, определен заранее — на этапе его написания, и, чтобы стандартный компонент мог вызывать метод (функцию) вашего свеженаписанного делегата (которого стандартному компоненту тоже нужно указать), этот метод(функция) должен соответствовать предопределённому методу. Если же делегат не определен, или у него не имплементирован како-то из методов, компонент должен сохранять работоспособность. То есть нужен этаки протокол обзения между объектами. В Objective-C набор таких предопределенных для делегата методов так и называется — Protocol. И когда вы проектируете класс, который может быть делегатом, вы принимаете этот протокол — то есть, ваш класс начинает знать прототипы функций, которые нужно имплементировать, и, собственно, пишете содержимое необходимых методов с заранее известными прототипами.

Это удобно.

Как сделать нечто подобное, когда у вас нет классов и объектов.
Не секрет, что в языке Си без всяких плюсов нет объектов. Но зато есть модули с функциями и данными, и сделать их максимально независимыми друг от друга всё же хочется. То есть сделать так, чтобы модулю, занимающемуся обработкой принятых данных, было безразлично откуда эти данные приходят — из SPI или I2C, или вообще генерируются усилиями CPU. А модулю, который принимает данные — безразлично куда он отправляет эти данные — просто ретранслирует их в консоль или через спутник прямо в Пентагон. Причём, хочется не только развязать модули, но и сделать так, чтобы имплементация калбэков была не обязательной, чтобы реализовывать только те функции стандартного компонента, которые нам нужны в текущей программе.

Слабый прицеп.
Похожий механизм можно сделать благодаря атрибутам функций weak (= слабый) и alias (= кличка, прозвище, замещение).
Употребление их вместе при объявлении функции говорит, что функция будет неприоритетно использоваться для замещения функции с другим названием, но таким же типом и аргументами.
Я накидал небольшой пример, чтобы протестировать эти возможности и понять самому, как с этим можно работать. Пример будет очень простой, но немножко жизненный. Допустим, мы хотим сделать стандартный компонент — системный таймер, который будет тикать с частотой, указанной при инициализации, при этом каждый тик (каждое переполнение таймера) будет выполнять свою логику + уведомлять другой неизвестный компонент о том, что он тикнул + передавать ему данные, специфичные для реализации таймера. «Делегатом» же (модулем, который будет имплементировать калбэк по желанию) будет выступать просто модуль, который в случае успешного вызова калбэка будет просто зажигать светодиоды.

Эмм. Архитектура.
То есть архитектура модулей вырисовывается примерно такая:
Architecture

То есть мы будем использовать CMSIS, как уровень абстракции над «железом». И напишем 2 модуля, которые будут работать так, как будто практически ничего не знают друг о друге.
Модуль SystemTimer будет имплементировать системный таймер со своей логикой, который будет дергать известную по прототипу и наванию функцию в неизвестном модуле (если таковая имеется).
Модуль Led будет зажигать светодиоды, если дернулся его метод — просто чтобы показать, что механизм работает.
Единственным связующим для этих двух модулей будет заголовок SystemTimerDelegate — в принципе, его может и не быть, но одна из целей тестирования такого механизма была в том, что модули должны уметь понимать специфические типы данных. В нашем случае это просто typedef над uint32_t, но гораздо интереснее использовать структуры для передачи сложных данных. В любом случае при наличии custom-типа данных, необходимо, чтобы его знал и модуль, которые эти данные собирается отправить, и модуль, который собирается эти данные обработать.

Итак, перейдём к сожержимому файлов.

SystemTimer.c

#include "SystemTimer.h"
#include "SystemTimerDelegate.h"
#include "cortexm/ExceptionHandlers.h"
#include "core_cm3.h"

// Empty alias function declaration for delegate replacement
void __attribute__ ((weak)) DefaultDelegate(systemTimerTick_t tick);
void __attribute__ ((weak, alias ("DefaultDelegate"))) SystemTimerDidCountTick(systemTimerTick_t tick);


// Private Declarations
static volatile systemTimerTick_t tickCounter = 0;
void SystemTimerTick (void);

// =========== Module Functions ===========
// Private Functions
void SystemTimerTick (void)
{
	tickCounter ++;
	SystemTimerDidCountTick(tickCounter);
}

// Public Functions
void SystemTimerInitWithFrequency (uint16_t herzFrequency)
{

	SysTick_Config (SystemCoreClock / herzFrequency);
}

void SystemTimerStart (void)
{
	/* Enable Timer */
	SysTick->CTRL  |= SysTick_CTRL_ENABLE_Msk;
}

void SystemTimerStop (void)
{
	/* Disable Timer */
	SysTick->CTRL  &= ~SysTick_CTRL_ENABLE_Msk;
}


// =========== Delegating Functions ===========
// Exception Handler Custom Implementation
void SysTick_Handler(void)
{
	GPIOC->BSRR = GPIO_BSRR_BR9;
	SystemTimerTick();
}

// =========== System Timer Alias Functions ===========
// Default Delegate Function Implementation
void DefaultDelegate(systemTimerTick_t tick)
{
	tick += 0;
}


Моментов, которые хотелось бы ответить здесь буквально чуть-чуть:
1. void __attribute__ ((weak)) DefaultDelegate(systemTimerTick_t tick); — объявление слабой функции, которая будет по-умолчанию вызываться вместо функии модуля-делегата, если такового модуля нет. То есть этим самым мы обеспечиваем работу модуля даже без имплементации объявленной функции-калбэка.
2. void __attribute__ ((weak, alias («DefaultDelegate»))) SystemTimerDidCountTick(systemTimerTick_t tick); — собственно, объявление того, что прототип с названием SystemTimerDidCountTick связан с DefaultDelegate.
3. В реализации функции void SystemTimerTick (void) строка tickCounter ++; — это логика самого SystemTimer, она отрабатывает всегда. SystemTimerDidCountTick(tickCounter); — это как раз калбэк, которого может и не быть. То есть если он есть, он вызывается, да ещё и с передачей параметра tickCounter, если же его нет — ничего страшного, будет вызвана функция DefaultDelegate.
4. В функции DefaultDelegate строка tick += 0; нужна лишь для того, чтобы компилятор лишний раз не ругался о не задействованных переменных. При оптимизации, она должна быть по идее помножена на нуль.
5. Вообще, сам SystemTimer является как-бы делегатом для модуля с векторами прерываний, имплементируя void SysTick_Handler(void).

SystemTimer.h

#ifndef SYSTEMTIMER_H_
#define SYSTEMTIMER_H_

#include "cmsis_device.h"
#include <stdint.h>

// Public Interface
void SystemTimerInitWithFrequency (uint16_t herzFrequency);
void SystemTimerStart (void);
void SystemTimerStop (void);

#endif /* SYSTEMTIMER_H_ */

Здесь отметить вообще нечего — это просто публичный интерфейс для нашего модуля.
Ну и здесь логично размещать #define-ы для настройки модуля.

SystemTimerDelegate.h

#ifndef SYSTEMTIMERDELEGATE_H_
#define SYSTEMTIMERDELEGATE_H_

#include <stdint.h>

// Special data structures for delegate functions
typedef uint32_t systemTimerTick_t;

// Delegate Functions
void SystemTimerDidCountTick(systemTimerTick_t tick);


#endif /* SYSTEMTIMERDELEGATE_H_ */

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

Led.c

#include "Led.h"

void LedInit (void)
{
	 // Enable GPIO Peripheral clock
	  RCC_APB2PeriphClockCmd(BLINK_RCC_MASKx(BLINK_PORT_NUMBER), ENABLE);

	  GPIO_InitTypeDef GPIO_InitStructure;

	  // Configure pin in output push/pull mode
	  GPIO_InitStructure.GPIO_Pin = BLINK_PIN_MASK(BLINK_PIN_NUMBER);
	  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	  GPIO_Init(BLINK_GPIOx(BLINK_PORT_NUMBER), &GPIO_InitStructure);

	  GPIO_InitStructure.GPIO_Pin = BLINK_PIN_MASK(BLINK_PIN_NUMBER_2);
	  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	  GPIO_Init(BLINK_GPIOx(BLINK_PORT_NUMBER), &GPIO_InitStructure);
}

void SystemTimerDidCountTick(systemTimerTick_t tick)
{
	LedOn();
}

Просто инициализация GPIO для зажигания светодиодов, и, собственно имплементация калбэка — то, ради чего весь сыр-бор. Здесь функция SystemTimerDidCountTick(systemTimerTick_t tick) просто зажигает светодиоды инлайновой функцией, определённой в хидере.

Led.h

#ifndef LED_H_
#define LED_H_

#include "stm32f10x.h"
#include "SystemTimerDelegate.h"


// Port numbers: 0=A, 1=B, 2=C, 3=D, 4=E, 5=F, 6=G, ...
#define BLINK_PORT_NUMBER               (2)
#define BLINK_PIN_NUMBER                (8)
#define BLINK_PIN_NUMBER_2              (9)
#define BLINK_ACTIVE_LOW                (1)

#define BLINK_GPIOx(_N)                 ((GPIO_TypeDef *)(GPIOA_BASE + (GPIOB_BASE-GPIOA_BASE)*(_N)))
#define BLINK_PIN_MASK(_N)              (1 << (_N))
#define BLINK_RCC_MASKx(_N)             (RCC_APB2Periph_GPIOA << (_N))
// ----------------------------------------------------------------------------

extern void LedInit(void);

// ----------------------------------------------------------------------------

inline void LedOn(void);

inline void LedOff(void);

// ----------------------------------------------------------------------------

inline void __attribute__((always_inline)) LedOn(void)
{
#if (BLINK_ACTIVE_LOW)
  GPIO_SetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
      BLINK_PIN_MASK(BLINK_PIN_NUMBER));
  GPIO_SetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
        BLINK_PIN_MASK(BLINK_PIN_NUMBER_2));
#else
  GPIO_SetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
      BLINK_PIN_MASK(BLINK_PIN_NUMBER));
  GPIO_ResetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
        BLINK_PIN_MASK(BLINK_PIN_NUMBER_2));
#endif
}

inline void __attribute__((always_inline)) LedOff(void)
{
#if (BLINK_ACTIVE_LOW)
  GPIO_ResetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
      BLINK_PIN_MASK(BLINK_PIN_NUMBER));
  GPIO_ResetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
        BLINK_PIN_MASK(BLINK_PIN_NUMBER_2));
#else
  GPIO_ResetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
      BLINK_PIN_MASK(BLINK_PIN_NUMBER));
  GPIO_SetBits(BLINK_GPIOx(BLINK_PORT_NUMBER),
        BLINK_PIN_MASK(BLINK_PIN_NUMBER_2));
#endif
}

#endif /* LED_H_ */

Led.h практически полностью утащен из демонстрационного проекта Eclipse, так как ничего важного в нём вообще нет.

Ну и, конечно же main.c

#include <stdio.h>
#include <stdint.h>
#include "diag/Trace.h"
#include "SystemTimer.h"
#include "Led.h"

// ----- main() ---------------------------------------------------------------
int main(int argc, char* argv[])
{
  // At this stage the system clock should have already been configured
  // at high speed.
	SystemTimerInitWithFrequency(1000);
	LedInit();
  // Infinite loop
  while (1)
    {
       // Add your code here.
    }
}

Как видите, кода здесь нет вообще. Инициализируем 2 модуля и больше не делаем ничего.

И чё?
Проверить то, что механизм работает можно, комментируя или раскомментируя функцию void SystemTimerDidCountTick(systemTimerTick_t tick) в модуле Led.c. Не пишем её — и системный таймер работает автономно, отрабатывая только свою логику. Пишем, и тут же начинаем получать калбэки от работающего системного таймера, например, для запуска менеджера задач, или мало ли ещё чего.

Самое крутое, что я лично из этого вынес — это то, что мы можем проектировать модули, вызывающие у абсолютно неизвестных других модулей самые неожиданные функции, НО! со вполне известными прототипами. При этом, наличие или не наличие этих функций у других модулей никак не нарушает работоспособность проектируемого модуля (ну только если в калбэке нет грубых ошибок), и это можно использовать для проектирования мало-связанных модулей, которые легко переиспользовать в разных проектах. Единственное неприятное ограничение, это то, что нельзя назначить делегата, то если в каком-то модуле реализована функция с объявленным прототипом, то она вызовется.

Мне кажется, это здорово.

Если вам статья понравилась, пишите в комментариях.
Если нет, и/или вы считаете всё вышеописанное бредом/чушью/детским лепетом — тоже пишите, я только учусь)

В любом случае, спасибо за прочтение.

Stay Heavy! \m/

Комментарии (17)

RSS свернуть / развернуть
Я правильно понимаю, что «weak/alias» не входят в стандарт C и являются фишками препроцессора GCC?
0
Да, я забыл написать, что написанное справедливо для gcc.
0
Да, я забыл написать, что написанное справедливо для gcc.
Для Keil-а тоже справедливо и для IAR ARM. Так что почти стандарт…
0
Weak – очень опасная штука.

Пример:

void SystemTemerDidCountTick(systemTimerTick_t tick)
{
        LedOn();
}

Я сознательно допустил опечатку в названии метода (Temer). Код скомплится? – да без ошибок и варнингов. И даже работать будет. Но не так как Вы ожидаете, «делегат» вызываться не будет. Удачной отладки в большом проекте :) Особенно весело, когда над проектом работает много человек. Я отрефакторил кусок кода с объявлением делегата, а у кого-то что-то перестало вызываться, что раньше работало.

Если нужен подобный функционал – я бы сделал по старинке, через указатили на функции и явное назначение «делегатов-калбеков». Плюс, такой подход более гибкий – можно в рантайме переназначать «делегатов». Вызов функции по указателю – очень незначительные накладные расходы.
+2
  • avatar
  • e_mc2
  • 21 октября 2014, 00:00
Да, опасность есть, я об этом не подумал сразу, но warning при ошибке в имени функции вылетает:
— no previous prototype for 'SystemTemerDidCountTick' [-Wmissing-prototypes]
0
Кстати, при имплементации протокола в Objective-C тоже есть опасность использования неправильного имени метода, и мы можем увидеть тоже только warning.
Но там в самой среде (Xcode) есть автодополнение имён методов, по-этому такие ошибки весьма редки.
Ещё один пример, где нужна исключительно внимательность — это написание имени метода при использовании KVO (Key-Value Observing).
Но возможность динамического изменения калбэка при использовании указателей на функцию — это реально крутая штука, тут не поспорить)
0
— no previous prototype for 'SystemTemerDidCountTick' [-Wmissing-prototypes]

Да, я забыл про -Wmissing-prototypes
Но, по моему, этот ворнинг по умолчанию отключен и в –Wall не входит? Или я ошибаюсь?

Кстати, при имплементации протокола в Objective-C тоже есть опасность использования неправильного имени метода, и мы можем увидеть тоже только warning.

Подобные вещи иногда встречаются. В С++ из за опечатки можно создать новый метод вместо переопределения родительского. Но от этого стараются уходить, в С++11 уже можно детектировать такие вещи средствами языка.
0
Да, в –Wall не входит, нужно отдельно включить.
0
Сугубо ИМХО, но (как советуют ниже в комментариях) если есть возможность выбирать инструмент – то лучше подобные вещи делать на С++. Тем более, у Вас есть опыт с Objective-C, и я не думаю, что освоение С++ будет для Вас проблемой.
Реализовать принципы ООП можно и средствами С, но я, честно говоря, не вижу в этом особого смысла.
0
Совет — возьмите C++ и сделайте все это на шаблонах. Сейчас участвую в проекте (большом), где решили во чтобы то ни стало обойтись чистым Си. В итоге несколько человек фул-тайм изобретают свой язык с объектами, виртуальными функциями, генериками и блекджеком, из которого кусками генерируется Си-код, остальное надо дописывать руками. Душераздирающее зрелище.

Конкретно по этой статье: настройки в хедере — плохая идея, хоть все так и делают. Проблемы при обновлении кода. Выносите в отдельный файл. Ну или как параметры шаблонов:

static DigitalOutputPort<PortB, 8> led;

class LedTask : public Task
{
  public:
    void execute() {
      led.toggle();
      nextStepMs(1000);
    }
};
static LedTask ledTask;

int main(void) {
  TaskController::run();
}
+1
  • avatar
  • dev
  • 21 октября 2014, 00:25
В итоге несколько человек фул-тайм изобретают свой язык...
В качестве примера реализации ООП на чистом «C» можно порекомендовать глянуть книги серии O'Reilly «Definitive Guides to the X Window System» и исходники Xt, Xaw, Xm.
Пример самодельного виджета:
TabBook.html
TabBook.tar.gz
0
Еще можно посмотреть GObject.
0
А мне нравится идея. Только вот лично мне при чтении такого кода было бы понять архитектуру тяжелее, чем простые коллбеки.
Действительно, пишите на плюсах, если ничто не ограничивает.

Естественно, говоря о Си, мы не можем говорить об объектах
Почему?

Ну и попридираюсь.
// Private Functions
void SystemTimerTick (void)
static?

inline void LedOn(void);
...
inline void __attribute__((always_inline)) LedOn(void)

Лучше static inline и без прототипа.
0
Признаться, я плохо знаю плюсЫ и боюсь набыдлокодить очень неэффективный код.
Пока разбираюсь с Си, периодически рассматривая ассемблерные партянки в дебаггере, чтобы понимать, как это все работает и сопоставить положительные и отрицательные стороны того или иного решения)
По-этому пока чистые Си. Но погружение в плюсЫ уже тщательно планирую)

Ну в Си, всё же, довольно сложно говорить о полноценных объектах, или есть какие-то общеизвестные best practice, с этим связанные?

За поправки спасибо!
0
Ну в Си, всё же, довольно сложно говорить о полноценных объектах, или есть какие-то общеизвестные best practice, с этим связанные?
В C++ ООП реализовано на уровне языка, а в C для этого придётся вводить некоторые соглашения и ограничения и добровольно соблюдать их.
В качестве примера best practices.
0
ВНЕЗАПНО, почему-то Линус люто ненавидит C++ и в открытую материт его:

C++ is a horrible language

Линус Торвальдс унижает C++
0
Ну, во первых, Линус — тот еще тролль. Даже в списке «известные люди, использующие EMACS», который я как-то нагуглил, фоточка Линуса взята та, где он говорит «FUCK YOU, NVIDIA!» :)
А во вторых — он вполне прав. Только альтернатив С++ в определенных областях все равно нет.
В третьих — ИМХО половина перечисленных минусов свойственна самому С в не меньшей мере.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.