offsetof(s, m)

Опубликовано в рубрике "Статьи", 1 сентября, 2010.
Тэги: , , , автор:

Если порыться по стандартным заголовочным фалам ANSI-C компилятора, то в файле stddef.h можно найти макрос offsetof()

Макрос offsetof() имеет тру-эмбеддерское объявление

1// у кейла
2#define offsetof(s, m) (size_t)&(((s *)0)-›m)
3
4// у IAR’а
5#define offsetof(T, m) (__INTADDR__((&((T *)0)-›m)))

Самое интересное, что несмотря на полезность этого макроса, он очень редко используется. Я и сам не знал о его существовании до сегодняшнего дня.

Надеюсь, из названия всем понятно, что делает этот макрос. Кому не понятно, объясняю. Он возвращает смещение поля в структуре. К примеру, есть структура:

1#pragma pack(push, 1)
2struct SomeStruct
3{
4 uint8 a;
5 uint16 b;
6 uint8 c;
7}
8
9// offsetof(SomeStruct, a) вернет 0,
10// offsetof(SomeStruct, b) вернет 1
11// offsetof(SomeStruct, c) вернет 3

Для того, чтобы понять, как offsetof() работает, рассмотрим по частям, как он разворачивается компилятором.  Для примера, возьмем макрос от Кейла:

1#define offsetof(s, m) (size_t)&(((s *)0)-›m)
  • ((s *)0) Приводит число ноль к указателю на структуру s. Эта строчка говорит компилятору, что  по адресу 0 располагается структура, и мы получаем указатель на нее.
  • ((s *)0)->m получает член m структуры s, компилятор будет думать, что этот член расположен по адресу 0 + смещение m
  • &(((s *)0)->m) вычисляем адрес члена m.
  • (size_t)&(((s *)0)->m) преобразовываем адрес члена m к целому числу.

 

Член m может быть любой сложности. К примеру, можно использовать вложенные структуры.

1struct AnotherStruct
2{
3 uint8 m;
4 uint8 n;
5};
6
7struct SomeStruct
8{
9 uint8 a;
10 uint16 b;
11 uint8 c;
12 AnotherStruct another_struct;
13}
14
15offsetof(SomeStruct, another_struct.m);

 

offsetof() так-же работает с union’ами

Теперь, кода вы понимаете, как работает этот макрос, попробуем привести пару реальных примеров его использования.

 

1. Чтение из энергонезависимой памяти

Для хранения настроек, многие микроконтроллерные системы содержат некую энергонезависимую
память, обычно это — EEPROM с последовательным интерфейсом.  Обычно, драйвер EEPROM’а для чтения предоставляет функцию типа вот такой:

1EepromRead(uint32 offset, uint32 count, uint8 *dest);

Естественно, возникает вопрос — какое смещение у запрашиваемой переменной от начала EEPROM. Типичное решение такой задачи — объявить структуру, которая повторяет содержимое EEPROM’а, объявить указатель на нее, и присвоить ему нулевой адрес

1struct Eeprom
2{
3 uint32 i;
4 float f;
5 uint8 c;
6};
7
8const Eeprom *const p_eeprom = 0x0000000;
9EepromRead(&(p_eeprom-›f), sizeof(p_eeprom-›f), dest);

Как видно, выглядит довольно запутанно. Немного получше использовать offsetof()

1struct Eeprom
2{
3 uint32 i;
4 float f;
5 uint8 c;
6};
7
8EepromRead(offsetof(Eeprom, f), 4, dest);

Остается проблема с размером, который придется вводить в ручную. Для ее решения, можно по образу и подобию offsetof() написать макрос SIZE_OF_MEMBER():

1#define SIZE_OF_MEMBER(s,m) ((size_t) sizeof(((s *)0)-›m));

теперь, чтение выглядит вот так

1EepromRead(offsetof(Eeprom, f), SIZE_OF_MEMBER(Eeprom, f), &dest);

Виден повторяющийся кусочек "EEPROM, f", чтение можно еще упростить, объявив макрос

1#define EEPROM_READ(M,D) EepromRead(offsetof(Eeprom, M), SIZE_OF_MEMBER(Eeprom, M), D);

В итоге, чтение становится совсем простым:

1EEPROM_READ(f, &dest);

Это то, что мы и хотели сказать компилятору "считай переменную f из EEPROM, и сохрани ее по адресу dest"

 

2. Защита энергонезависимой памяти

Много микроконтроллерных систем содержат память, содержимое которой может быть повреждено.
Пример такой памяти — ОЗУ с батарейным питанием в LPC2xxx или LPC1xxx.

Задача простая — нужно обнаружить — было ли повреждено содержимое такой памяти или нет. Для этого я добавляю в структуру, хранящуюся в такой памяти поле с CRC. Таким образом, обычно получается что-то вот такое:

1struct Nvm
2{
3 uint32 i;
4 uint8 f;
5 uint8 c;
6 uint16 crc;
7};
8
9Nvm nvram;

 

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

1nvram.crc = crc16((char *)&nvram, sizeof(nvram)-sizeof(nvram.crc));

Такой код будет работать только, если компилятор выравнивает данные по байтовой границе. Если данные будут выровнены по 4 байтовой границе (arm7, к примеру), то структура глазами  компилятора будет выглядеть так:

1struct Nvm
2{
3 uint32 i; — смещение 0
4 uint8 f; — смещение 4
5 uint8 pad1[3];
6 uint8 c; — смещение 8
7 uint8 pad2[3];
8 uint16 crc; — смещение 12
9 uint8 pad[2];
10};

 

pad- байты, которые не использует приложение. Они вставлены для организации выравнивания.

Таким образом, sizeof(nvram) = 16, а sizeof(nvram.crc) = 2. В результате, CRC будет рассчитываться с использованием своего старого значения.  Упс.

Конечно-же, можно запаковать структуру плотнее с помощью #pragma pack, но когда это не желательно или невозможно, лучше использовать offsetof()

1nvram.crc = crc16((uint8_t *) &nvram, offsetof(Nvm, crc));

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

А где offsetof() используете вы? 🙂




Комментарии
  1. google.com/profiles/gm… написал(а) 19 октября, 2010 в 12:48

    Увидев в строке
    #define offsetof(s, m) (size_t)&(((s *)0)->m)
    жуткие «&» и «->», взгрустнул от своей необразованности и не «тру-эмбеддерости». Хорошо бы исправить.
    Далее в тексте эта строка показана нормально.
    Также, не все в курсе что такое size_t в Keil’e.

    google.com/profiles/gm… Reply:

    Грм… А в комменте эти «amp» и «gt» рисуются нормально…

    BSVi Reply:

    Поправил. Нужно еще разобрваться с отображением openid ников. Даже и не представляю, где копать (

    size_t — это не из кейла, а из стандартной библиотеки C. Википедия знает — http://ru.wikipedia.org/wiki/Stddef.h#.D0.A2.D0.B8.D0.BF_size_t

    google.com/profiles/gm… Reply:

    Упс. Пардоньте… Прикольно что Википедия про size_t знает, а IAR — нет. 🙂

    BSVi Reply:

    IAR знает. Взгляните в файл %IAR_PATH%\avr\inc\clib\sysmac.h
    А еще, взялните сюда — http://bsvi.ru/rules/

    google.com/profiles/gm… Reply:

    Верю, ибо IAR у меня не установлен. Непонятно, чтож они тогда __INTADDR__ используют?

    BSVi Reply:

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

  2. YS написал(а) 16 марта, 2012 в 18:47

    >»Самое интересное, что несмотря на полезность этого макроса, он очень редко используется.»

    Правила MISRA C не рекомендуют использовать offsetof.

    BSVi Reply:

    Да мирса много чего не рекомендует. Ты попробуй пописать с соблюдением всех ее правил. Ты свихнешься сразу-же 🙂 Я пробовал. Ее нужно использовать только там, где отказ очень-очень критичен.

    YS Reply:

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

    Кстати, я про этот стандарт узнал недавно, и по прочтении с удивлением обнаружил, что практически точно ему следую. O_O

    BSVi Reply:

    очень странно, я, наоборот, очень сильно отклоняюсь. Попробовал писать в нем, оказалось жудко неудобно.

    YS Reply:

    Ну, у меня тоже отклонения есть, но, как я понял, немного.

    Даже интересно, а что Вас больше всего из правил раздражает (пару-тройку примеров, если несложно)? Интересно стили сравнить. 🙂

    Я читал правила в доках к IAR’у.

    BSVi Reply:

    Ссори, сейчас уже не помню. Помню просто, что траблов было много. Хотя, можно и привыкнуть. Я тоже использовал иар с включенной проверкой. Дак он ругался на все подряд 🙂

Создать новую ветку комментариев


Вы должны войти или зарегистрироваться чтобы оставить комментарий.