diff --git a/codegen/__main__.py b/codegen/__main__.py
index 7d6135a..fae93c7 100644
--- a/codegen/__main__.py
+++ b/codegen/__main__.py
@@ -1,5 +1,6 @@
-from .codegen import test_str8, test_typed_array
+from .codegen import test_str8, test_doubly_linked_list, test_typed_array
 
 if __name__ == "__main__":
     test_str8()
     test_typed_array()
+    test_doubly_linked_list()
diff --git a/codegen/codegen.py b/codegen/codegen.py
index 5b0a23f..87a29fd 100644
--- a/codegen/codegen.py
+++ b/codegen/codegen.py
@@ -1,5 +1,6 @@
 from pathlib import Path
 from .datatypes import (
+    CDataType,
     CStruct,
     CEnum,
     CEnumVal,
@@ -13,6 +14,7 @@ from .datatypes import (
     CQualifier,
     CInclude,
     CUserType,
+    get_datatype_string,
 )
 
 
@@ -149,3 +151,196 @@ def test_typed_array():
 
     header.save(Path("."))
     source.save(Path("."))
+
+
+def test_doubly_linked_list():
+    datatypes: dict[CDataType, list[CInclude]] = {"Str8": [CInclude(header="str8.h", local=True)]}
+    snippets_dir = Path("snippets")
+
+    for _type, includes in datatypes.items():
+        type_string = get_datatype_string(_type)
+
+        node = CStruct(
+            name=f"{type_string}Node",
+            cargs=[
+                CArg(name="string", _type=type_string, pointer=CPointer(_type=CPointerType.SINGLE)),
+                CArg(name="prev", _type=f"{type_string}Node", pointer=CPointer(_type=CPointerType.SINGLE)),
+                CArg(name="next", _type=f"{type_string}Node", pointer=CPointer(_type=CPointerType.SINGLE)),
+            ],
+        )
+        
+        dl_list = CStruct(
+            name=f"{type_string}List",
+            cargs=[
+                CArg(name="first", _type=node, pointer=CPointer(_type=CPointerType.SINGLE)),
+                CArg(name="last", _type=node, pointer=CPointer(_type=CPointerType.SINGLE)),
+                CArg(name="total_size", _type=CType.U64),
+                CArg(name="node_count", _type=CType.U64),
+            ],
+        )
+
+        get_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_get",
+            ret_type=node,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE), qualifier=CQualifier.CONST),
+                CArg(name="index", _type=CType.U64),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_get").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+            pointer=CPointer(CPointerType.SINGLE),
+        )
+
+        push_front_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_push_front",
+            ret_type=CType.VOID,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+                CArg(name="node", _type=node, pointer=CPointer(CPointerType.SINGLE)),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_push_front").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+        )
+
+        push_back_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_push_back",
+            ret_type=CType.VOID,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+                CArg(name="node", _type=node, pointer=CPointer(CPointerType.SINGLE)),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_push_back").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+        )
+
+        insert_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_insert",
+            ret_type=CType.VOID,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+                CArg(name="node", _type=node, pointer=CPointer(CPointerType.SINGLE)),
+                CArg(name="index", _type=CType.U64),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_insert").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+        )
+
+        pop_front_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_pop_front",
+            ret_type=node,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_pop_front").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+            pointer=CPointer(CPointerType.SINGLE),
+        )
+
+        pop_back_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_pop_back",
+            ret_type=node,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_pop_back").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+            pointer=CPointer(CPointerType.SINGLE),
+        )
+
+        remove_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_remove",
+            ret_type=node,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+                CArg(name="index", _type=CType.U64),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_remove").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+            pointer=CPointer(CPointerType.SINGLE),
+        )
+
+        empty_func = CFunc(
+            name=f"wapp_{type_string.lower()}_list_empty",
+            ret_type=CType.VOID,
+            args=[
+                CArg(name="list", _type=dl_list, pointer=CPointer(CPointerType.SINGLE)),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "list_empty").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower()
+            ),
+        )
+
+        node_to_list_func = CFunc(
+            name=f"{type_string.lower()}_node_to_list",
+            ret_type=dl_list,
+            args=[
+                CArg(name="node", _type=node, pointer=CPointer(CPointerType.SINGLE)),
+            ],
+            body=load_func_body_from_file(Path(__file__).parent / snippets_dir / "node_to_list").format(
+                T=type_string,
+                Tupper=type_string.upper(),
+                Tlower=type_string.lower(),
+            ),
+            qualifiers=[CQualifier.INTERNAL],
+        )
+
+        header = CHeader(
+            name=f"{type_string.lower()}_list",
+            includes=[CInclude(header="aliases.h", local=True)],
+            types=[node, dl_list],
+            funcs=[
+                get_func,
+                push_front_func,
+                push_back_func,
+                insert_func,
+                pop_front_func,
+                pop_back_func,
+                remove_func,
+                empty_func,
+           ]
+        )
+
+        source = CSource(
+            name=header.name,
+            includes=[CInclude(header="aliases.h", local=True), CInclude(header, local=True), CInclude(header="stddef.h")],
+            internal_funcs=[node_to_list_func],
+            funcs=header.funcs
+        )
+
+        if len(includes) > 0:
+            header.includes.extend(includes)
+            source.includes.extend(includes)
+
+        header.includes = sorted(header.includes, key=lambda inc: inc.local, reverse=True)
+        source.includes = sorted(source.includes, key=lambda inc: inc.local, reverse=True)
+
+        header.save(Path("."))
+        source.save(Path("."))
+
+
+def load_func_body_from_file(filename: Path) -> str:
+    with open(filename, "r") as infile:
+        return infile.read()
diff --git a/codegen/snippets/list_empty b/codegen/snippets/list_empty
new file mode 100644
index 0000000..c14804f
--- /dev/null
+++ b/codegen/snippets/list_empty
@@ -0,0 +1,8 @@
+  if (!list) {{
+    return;
+  }}
+
+  u64 count = list->node_count;
+  for (u64 i = 0; i < count; ++i) {{
+    wapp_str8_list_pop_back(list);
+  }}
diff --git a/codegen/snippets/list_get b/codegen/snippets/list_get
new file mode 100644
index 0000000..c65985e
--- /dev/null
+++ b/codegen/snippets/list_get
@@ -0,0 +1,13 @@
+  if (index >= list->node_count) {{
+    return NULL;
+  }}
+
+  {T}Node *output  = NULL;
+  {T}Node *current = list->first;
+  for (u64 i = 1; i <= index; ++i) {{
+    current = current->next;
+  }}
+
+  output = current;
+
+  return output;
diff --git a/codegen/snippets/list_insert b/codegen/snippets/list_insert
new file mode 100644
index 0000000..0b6aa52
--- /dev/null
+++ b/codegen/snippets/list_insert
@@ -0,0 +1,29 @@
+  if (!list || !node || !(node->string)) {{
+    return;
+  }}
+
+  if (index == 0) {{
+    wapp_str8_list_push_front(list, node);
+    return;
+  }} else if (index == list->node_count) {{
+    wapp_str8_list_push_back(list, node);
+    return;
+  }}
+
+  {T}Node *dst_node = wapp_str8_list_get(list, index);
+  if (!dst_node) {{
+    return;
+  }}
+
+  {T}List node_list = {Tlower}_node_to_list(node);
+
+  list->total_size += node_list.total_size;
+  list->node_count += node_list.node_count;
+
+  {T}Node *prev = dst_node->prev;
+
+  dst_node->prev = node_list.last;
+  prev->next     = node_list.first;
+
+  node_list.first->prev = prev;
+  node_list.last->next  = dst_node;
diff --git a/codegen/snippets/list_pop_back b/codegen/snippets/list_pop_back
new file mode 100644
index 0000000..3ee8a15
--- /dev/null
+++ b/codegen/snippets/list_pop_back
@@ -0,0 +1,21 @@
+  {T}Node *output = NULL;
+
+  if (!list || list->node_count == 0) {{
+    goto RETURN_{Tupper}_LIST_POP_BACK;
+  }}
+
+  output = list->last;
+
+  if (list->node_count == 1) {{
+    *list = ({T}List){{0}};
+    goto RETURN_{Tupper}_LIST_POP_BACK;
+  }}
+
+  --(list->node_count);
+  list->total_size -= output->string->size;
+  list->last        = output->prev;
+
+  output->prev = output->next = NULL;
+
+RETURN_{Tupper}_LIST_POP_BACK:
+  return output;
diff --git a/codegen/snippets/list_pop_front b/codegen/snippets/list_pop_front
new file mode 100644
index 0000000..caba8c9
--- /dev/null
+++ b/codegen/snippets/list_pop_front
@@ -0,0 +1,21 @@
+  {T}Node *output = NULL;
+
+  if (!list || list->node_count == 0) {{
+    goto RETURN_{Tupper}_LIST_POP_FRONT;
+  }}
+
+  output = list->first;
+
+  if (list->node_count == 1) {{
+    *list = ({T}List){{0}};
+    goto RETURN_{Tupper}_LIST_POP_FRONT;
+  }}
+
+  --(list->node_count);
+  list->total_size -= output->string->size;
+  list->first       = output->next;
+
+  output->prev = output->next = NULL;
+
+RETURN_{Tupper}_LIST_POP_FRONT:
+  return output;
diff --git a/codegen/snippets/list_push_back b/codegen/snippets/list_push_back
new file mode 100644
index 0000000..cc51dac
--- /dev/null
+++ b/codegen/snippets/list_push_back
@@ -0,0 +1,21 @@
+  if (!list || !node || !(node->string)) {{
+    return;
+  }}
+
+  {T}List node_list = {Tlower}_node_to_list(node);
+
+  if (list->node_count == 0) {{
+    *list = node_list;
+    return;
+  }}
+
+  list->total_size += node_list.total_size;
+  list->node_count += node_list.node_count;
+
+  {T}Node *last = list->last;
+  if (last) {{
+    last->next = node_list.first;
+  }}
+
+  list->last            = node_list.last;
+  node_list.first->prev = last;
diff --git a/codegen/snippets/list_push_front b/codegen/snippets/list_push_front
new file mode 100644
index 0000000..8989beb
--- /dev/null
+++ b/codegen/snippets/list_push_front
@@ -0,0 +1,21 @@
+  if (!list || !node || !(node->string)) {{
+    return;
+  }}
+
+  {T}List node_list = {Tlower}_node_to_list(node);
+
+  if (list->node_count == 0) {{
+    *list = node_list;
+    return;
+  }}
+
+  list->total_size += node_list.total_size;
+  list->node_count += node_list.node_count;
+
+  {T}Node *first = list->first;
+  if (first) {{
+    first->prev = node_list.last;
+  }}
+
+  list->first          = node_list.first;
+  node_list.last->next = first;
diff --git a/codegen/snippets/list_remove b/codegen/snippets/list_remove
new file mode 100644
index 0000000..cc213c4
--- /dev/null
+++ b/codegen/snippets/list_remove
@@ -0,0 +1,28 @@
+  {T}Node *output = NULL;
+  if (!list) {{
+    goto RETURN_{Tupper}_LIST_REMOVE;
+  }}
+
+  if (index == 0) {{
+    output = wapp_str8_list_pop_front(list);
+    goto RETURN_{Tupper}_LIST_REMOVE;
+  }} else if (index == list->node_count) {{
+    output = wapp_str8_list_pop_back(list);
+    goto RETURN_{Tupper}_LIST_REMOVE;
+  }}
+
+  output = wapp_str8_list_get(list, index);
+  if (!output) {{
+    goto RETURN_{Tupper}_LIST_REMOVE;
+  }}
+
+  output->prev->next = output->next;
+  output->next->prev = output->prev;
+
+  --(list->node_count);
+  list->total_size -= output->string->size;
+
+  output->prev = output->next = NULL;
+
+RETURN_{Tupper}_LIST_REMOVE:
+  return output;
diff --git a/codegen/snippets/node_to_list b/codegen/snippets/node_to_list
new file mode 100644
index 0000000..2c52609
--- /dev/null
+++ b/codegen/snippets/node_to_list
@@ -0,0 +1,15 @@
+  {T}List output = {{.first = node, .last = node, .total_size = node->string->size, .node_count = 1}};
+
+  while (output.first->prev != NULL) {{
+    output.total_size += output.first->prev->string->size;
+    output.first = output.first->prev;
+    ++(output.node_count);
+  }}
+
+  while (output.last->next != NULL) {{
+    output.total_size += output.last->next->string->size;
+    output.last = output.last->next;
+    ++(output.node_count);
+  }}
+
+  return output;