Skip to content

Commit

Permalink
misc: add linked list helpers
Browse files Browse the repository at this point in the history
This provides macros for managing intrusive doubly linked lists.

There are many ways how to do those in a "generic" way in C. For example
Solaris style lists are pretty nice:

https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/list.h
https://github.com/illumos/illumos-gate/blob/master/usr/src/common/list/list.c

I even have an independent implementation of this, which could be ISC
licensed. But I think it's easier to vomit ~100 lines of preprocessor
garbage, which has a lower footprint, and I think it wins slightly on
the side of type safety, simplicity, and ease of use, even if it doesn't
look as magically nice.
  • Loading branch information
wm4 committed May 24, 2018
1 parent 094f6be commit 782e428
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 0 deletions.
107 changes: 107 additions & 0 deletions misc/linked_list.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#pragma once

#include <stddef.h>

/*
* Doubly linked list macros. All of these require that each list item is a
* struct, that contains a field, that is another struct with prev/next fields:
*
* struct example_item {
* struct {
* struct example_item *prev, *next;
* } mylist;
* };
*
* And a struct somewhere that represents the "list" and has head/tail fields:
*
* struct {
* struct example_item *head, *tail;
* } mylist_var;
*
* Then you can e.g. insert elements like this:
*
* struct example_item item;
* LL_APPEND(mylist, &mylist_var, &item);
*
* The first macro argument is always the name if the field in the item that
* contains the prev/next pointers, in this case struct example_item.mylist.
* This was done so that a single item can be in multiple lists.
*
* The list is started/terminated with NULL. Nothing ever points _to_ the
* list head, so the list head memory location can be safely moved.
*
* General rules are:
* - list head is initialized by setting head/tail to NULL
* - list items do not need to be initialized before inserting them
* - next/prev fields of list items are not cleared when they are removed
* - there's no way to know whether an item is in the list or not (unless
* you clear prev/next on init/removal, _and_ check whether items with
* prev/next==NULL are referenced by head/tail)
*/

// Insert item at the end of the list (list->tail == item).
// Undefined behavior if item is already in the list.
#define LL_APPEND(field, list, item) do { \
(item)->field.prev = (list)->tail; \
(item)->field.next = NULL; \
LL_RELINK_(field, list, item) \
} while (0)

// Insert item enew after eprev (i.e. eprev->next == enew). If eprev is NULL,
// then insert it as head (list->head == enew).
// Undefined behavior if enew is already in the list, or eprev isn't.
#define LL_INSERT_AFTER(field, list, eprev, enew) do { \
(enew)->field.prev = (eprev); \
(enew)->field.next = (eprev) ? (eprev)->field.next : (list)->head; \
LL_RELINK_(field, list, enew) \
} while (0)

// Insert item at the start of the list (list->head == item).
// Undefined behavior if item is already in the list.
#define LL_PREPEND(field, list, item) do { \
(item)->field.prev = NULL; \
(item)->field.next = (list)->head; \
LL_RELINK_(field, list, item) \
} while (0)

// Insert item enew before enext (i.e. enew->next == enext). If enext is NULL,
// then insert it as tail (list->tail == enew).
// Undefined behavior if enew is already in the list, or enext isn't.
#define LL_INSERT_BEFORE(field, list, enext, enew) do { \
(enew)->field.prev = (enext) ? (enext)->field.prev : (list)->tail; \
(enew)->field.next = (enext); \
LL_RELINK_(field, list, enew) \
} while (0)

// Remove the item from the list.
// Undefined behavior if item is not in the list.
#define LL_REMOVE(field, list, item) do { \
if ((item)->field.prev) { \
(item)->field.prev->field.next = (item)->field.next; \
} else { \
(list)->head = (item)->field.next; \
} \
if ((item)->field.next) { \
(item)->field.next->field.prev = (item)->field.prev; \
} else { \
(list)->tail = (item)->field.prev; \
} \
} while (0)

// Remove all items from the list.
#define LL_CLEAR(field, list) do { \
(list)->head = (list)->tail = NULL; \
} while (0)

// Internal helper.
#define LL_RELINK_(field, list, item) \
if ((item)->field.prev) { \
(item)->field.prev->field.next = (item); \
} else { \
(list)->head = (item); \
} \
if ((item)->field.next) { \
(item)->field.next->field.prev = (item); \
} else { \
(list)->tail = (item); \
}
162 changes: 162 additions & 0 deletions test/linked_list.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#include "test_helpers.h"

#include "common/common.h"
#include "misc/linked_list.h"

struct list_item {
int v;
struct {
struct list_item *prev, *next;
} list_node;
};

struct the_list {
struct list_item *head, *tail;
};

static bool do_check_list(struct the_list *lst, int *c, int num_c)
{
if (!lst->head)
assert_true(!lst->tail);
if (!lst->tail)
assert_true(!lst->head);

for (struct list_item *cur = lst->head; cur; cur = cur->list_node.next) {
if (cur->list_node.prev) {
assert_true(cur->list_node.prev->list_node.next == cur);
assert_true(lst->head != cur);
} else {
assert_true(lst->head == cur);
}
if (cur->list_node.next) {
assert_true(cur->list_node.next->list_node.prev == cur);
assert_true(lst->tail != cur);
} else {
assert_true(lst->tail == cur);
}

if (num_c < 1)
return false;
if (c[0] != cur->v)
return false;

num_c--;
c++;
}

if (num_c)
return false;

return true;
}

static void test_linked_list(void **state)
{
struct the_list lst = {0};
struct list_item e1 = {1};
struct list_item e2 = {2};
struct list_item e3 = {3};
struct list_item e4 = {4};
struct list_item e5 = {5};
struct list_item e6 = {6};

#define check_list(...) \
assert_true(do_check_list(&lst, (int[]){__VA_ARGS__}, \
sizeof((int[]){__VA_ARGS__}) / sizeof(int)));
#define check_list_empty() \
assert_true(do_check_list(&lst, NULL, 0));

check_list_empty();
LL_APPEND(list_node, &lst, &e1);

check_list(1);
LL_APPEND(list_node, &lst, &e2);

check_list(1, 2);
LL_APPEND(list_node, &lst, &e4);

check_list(1, 2, 4);
LL_CLEAR(list_node, &lst);

check_list_empty();
LL_PREPEND(list_node, &lst, &e4);

check_list(4);
LL_PREPEND(list_node, &lst, &e2);

check_list(2, 4);
LL_PREPEND(list_node, &lst, &e1);

check_list(1, 2, 4);
LL_CLEAR(list_node, &lst);

check_list_empty();
LL_INSERT_BEFORE(list_node, &lst, (struct list_item *)NULL, &e6);

check_list(6);
LL_INSERT_BEFORE(list_node, &lst, (struct list_item *)NULL, &e1);

check_list(6, 1);
LL_INSERT_BEFORE(list_node, &lst, (struct list_item *)NULL, &e2);

check_list(6, 1, 2);
LL_INSERT_BEFORE(list_node, &lst, &e6, &e3);

check_list(3, 6, 1, 2);
LL_INSERT_BEFORE(list_node, &lst, &e6, &e5);

check_list(3, 5, 6, 1, 2);
LL_INSERT_BEFORE(list_node, &lst, &e2, &e4);

check_list(3, 5, 6, 1, 4, 2);
LL_REMOVE(list_node, &lst, &e6);

check_list(3, 5, 1, 4, 2);
LL_REMOVE(list_node, &lst, &e3);

check_list(5, 1, 4, 2);
LL_REMOVE(list_node, &lst, &e2);

check_list(5, 1, 4);
LL_REMOVE(list_node, &lst, &e4);

check_list(5, 1);
LL_REMOVE(list_node, &lst, &e5);

check_list(1);
LL_REMOVE(list_node, &lst, &e1);

check_list_empty();
LL_APPEND(list_node, &lst, &e2);

check_list(2);
LL_REMOVE(list_node, &lst, &e2);

check_list_empty();
LL_INSERT_AFTER(list_node, &lst, (struct list_item *)NULL, &e1);

check_list(1);
LL_INSERT_AFTER(list_node, &lst, (struct list_item *)NULL, &e2);

check_list(2, 1);
LL_INSERT_AFTER(list_node, &lst, (struct list_item *)NULL, &e3);

check_list(3, 2, 1);
LL_INSERT_AFTER(list_node, &lst, &e3, &e4);

check_list(3, 4, 2, 1);
LL_INSERT_AFTER(list_node, &lst, &e4, &e5);

check_list(3, 4, 5, 2, 1);
LL_INSERT_AFTER(list_node, &lst, &e1, &e6);

check_list(3, 4, 5, 2, 1, 6);
}

int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_linked_list),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

0 comments on commit 782e428

Please sign in to comment.