container_of подробно

от
Прочие языки    си

https://annimon.com/code/?act=comm&id=4693 Поскольку возникли вопросы, как эта магия работает, разберём макрос подробнее.

task.png
Итак, есть задача: зная адрес вложенной структуры child (на рисунке выделен светло-зелёным оттенком, так я обозначаю известный адрес), нужно получить адрес структуры parent (выделено цветом, близким к оранжевому, неизвестный адрес), содержащей child. Можно было бы хранить указать на parent в child, но есть несколько проблем. Для примера были даны простейшие структуры, но в реальных задачах структуры могут содержать много элементов и хуже, если есть полный (или почти полный) граф указателей между ними, тогда можно запросто забыть проинициализировать указатель во вложенной структуре на структуру-контейнер, тем более когда вложенных структур несколько. Чтобы этого избежать, применяется арифметика указателей, и ниже подробнее рассмотрим, что же такого делает container_of. Для простоты возьмём первоначальный пример. Ниже я вновь приведу структуры и полный макрос, чтобы не нужно было переходить по ссылке в "полезные коды", а потом разберём его по кусочкам.
  1. #define container_of(ptr, type, member) \
  2.   ((type *)((char *)(ptr)-(char *)(&((type *)0)->member)))
  3. struct child {
  4.     int id;
  5. };
  6.  
  7. struct parent {
  8.     int id;
  9.     struct child child;
  10. };
1. Рассмотрим объявление макроса: #define container_of(ptr, type, member)
Здесь параметры: ptr - указатель на вложенную структуру. type - тип структуры контейнера и member - имя вложенной структуры.
Если задана структура
  1. struct parent {
  2.         int id;
  3.         struct child left;
  4.         struct child right;
  5. }
то получить указатель указатель на parent можно:
a) зная адрес (пусть будет (p_left) потомка left:
  1. parent = container_of(p_left, struct parent, left);
б) зная адрес (пусть будет (p_right) потомка right:
  1. parent = container_of(p_right, struct parent, right);
2. Рассмотрим часть, выделенную цветом: ((type *)((char *)(ptr)-(char *)(&((type *)0)->member))). Здесь мы говорим: "Вычисли мне адрес вложенной структуры, если бы контейнер размещался в памяти, начиная с ячейки номер 0x0". Таким образом мы получаем значение смещения потомка, относительно начала контейнера в байтах. В исходном примере первые четыре байта занимает поле parent.id, а значит, что если бы parent располагался в памяти, начиная с адреса 0x0, то parent.child начиналось бы с адреса 0x4 и было бы смещено относительно начала контейнера на 4 байта.
  beginning.png
3. Рассмотрим следующий фрагмент: ((type *)(char *)(ptr)-(char *)(&((type *)0)->member))
Зная на сколько байт смещено вложенное поле, мы можем получить адрес начала контейнера, вычитав из адреса child его смещение. Но поскольку нам нужно вычитать именно количество байт, оба указателя приводятся к char *, потому что тип char занимает ровно один байт. Сравните два примера для char и int:
  1. char *p_char = (char *)0; //0x0
  2. p_char++; //0x1, потому что char занимает 1 байт
  3.  
  4. int *p_int = (int *)0; //0x0
  5. p_int++; //0x4, потому что int занимает в памяти 4 байта
4. И последнее: получившийся адрес приводится к типу контейнера:
(type *)((char *)(ptr)-(char *)(&((type *)0)->member)
Всё :)
Вопрос на закуску: можно ли изменить смещение child относительно начала контейнера, не изменяя при этом структуру parent? На всякий случай: считаем, что компиляция производится только под одну платформу, так что вариант "перекомпилировать под платформу, где int занимает 2 байта" не рассматривается.
  • +6
  • views 4268