diff --git a/.gitignore b/.gitignore
index 920b172..b7b3e1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,9 @@ cmake-build-release/
 .idea/
 node_modules/
 *.dSYM
-.vs/
\ No newline at end of file
+.vs/
+bindings/odin/clay-odin/tmp/
+
+generator/__pycache__/
+
+generator/generators/__pycache__/
diff --git a/generator/__init__.py b/generator/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/generator/cli.py b/generator/cli.py
new file mode 100644
index 0000000..0eb8457
--- /dev/null
+++ b/generator/cli.py
@@ -0,0 +1,83 @@
+import argparse
+import logging
+import json
+
+from pathlib import Path
+
+from generators.base_generator import BaseGenerator
+from generators.odin_generator import OdinGenerator
+from parser import parse_headers
+
+logger = logging.getLogger(__name__)
+
+GeneratorMap = dict[str, type[BaseGenerator]]
+GENERATORS = {
+    'odin': OdinGenerator,
+}
+
+def main() -> None:
+    arg_parser = argparse.ArgumentParser(description='Generate clay bindings')
+
+    # Directories
+    arg_parser.add_argument('input_files', nargs='+', type=str, help='Input header files')
+    arg_parser.add_argument('--output-dir', type=str, help='Output directory', required=True)
+    arg_parser.add_argument('--tmp-dir', type=str, help='Temporary directory')
+    
+    # Generators
+    arg_parser.add_argument('--generator', type=str, choices=list(GENERATORS.keys()), help='Generators to run', required=True)
+
+    # Logging
+    arg_parser.add_argument('--verbose', action='store_true', help='Verbose logging')
+
+    args = arg_parser.parse_args()
+
+    log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+    log_handler = logging.StreamHandler()
+    log_handler.setFormatter(log_formatter)
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG, handlers=[log_handler])
+    else:
+        logging.basicConfig(level=logging.INFO, handlers=[log_handler])
+
+    output_dir = Path(args.output_dir)
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    if args.tmp_dir:
+        tmp_dir = Path(args.tmp_dir)
+    else:
+        tmp_dir = output_dir / 'tmp'
+    tmp_dir.mkdir(parents=True, exist_ok=True)
+
+    fake_libc_include_path = Path(__file__).parent / 'fake_libc_include'
+    input_files = list(fake_libc_include_path.glob('*.h')) + [Path(f) for f in args.input_files]
+
+    logger.info(f'Input files: {input_files}')
+    logger.info(f'Output directory: {output_dir}')
+    logger.info(f'Temporary directory: {tmp_dir}')
+    logger.info(f'Generator: {args.generator}')
+
+    logger.info('Parsing headers')
+    extracted_symbols = parse_headers(input_files, tmp_dir)
+    with open(tmp_dir / 'extracted_symbols.json', 'w') as f:
+        f.write(json.dumps({
+            'structs': extracted_symbols.structs,
+            'enums': extracted_symbols.enums,
+            'functions': extracted_symbols.functions,
+        }, indent=2))
+
+    logger.info('Generating bindings')
+    generator = GENERATORS[args.generator](extracted_symbols)
+    generator.generate()
+    logger.debug(f'Generated bindings:')
+    # for file_name, content in generator.get_outputs().items():
+    #     logger.debug(f'{file_name}:')
+    #     logger.debug(content)
+    #     logger.debug('\n')
+    
+    tmp_outputs_dir = tmp_dir / 'generated'
+    tmp_outputs_dir.mkdir(parents=True, exist_ok=True)
+    generator.write_outputs(tmp_outputs_dir)
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/generator/fake_libc_include/_fake_defines.h b/generator/fake_libc_include/_fake_defines.h
new file mode 100644
index 0000000..852744a
--- /dev/null
+++ b/generator/fake_libc_include/_fake_defines.h
@@ -0,0 +1,262 @@
+#ifndef _FAKE_DEFINES_H
+#define _FAKE_DEFINES_H
+
+#define	NULL	0
+#define	BUFSIZ		1024
+#define	FOPEN_MAX	20
+#define	FILENAME_MAX	1024
+
+#ifndef SEEK_SET
+#define	SEEK_SET	0	/* set file offset to offset */
+#endif
+#ifndef SEEK_CUR
+#define	SEEK_CUR	1	/* set file offset to current plus offset */
+#endif
+#ifndef SEEK_END
+#define	SEEK_END	2	/* set file offset to EOF plus offset */
+#endif
+
+#define __LITTLE_ENDIAN 1234
+#define LITTLE_ENDIAN __LITTLE_ENDIAN
+#define __BIG_ENDIAN 4321
+#define BIG_ENDIAN __BIG_ENDIAN
+#define __BYTE_ORDER __LITTLE_ENDIAN
+#define BYTE_ORDER __BYTE_ORDER
+
+#define EXIT_FAILURE 1
+#define EXIT_SUCCESS 0
+
+#define SCHAR_MIN -128
+#define SCHAR_MAX 127
+#define CHAR_MIN -128
+#define CHAR_MAX 127
+#define UCHAR_MAX 255
+#define SHRT_MIN -32768
+#define SHRT_MAX 32767
+#define USHRT_MAX 65535
+#define INT_MIN -2147483648
+#define INT_MAX 2147483647
+#define UINT_MAX 4294967295U
+#define LONG_MIN -9223372036854775808L
+#define LONG_MAX 9223372036854775807L
+#define ULONG_MAX 18446744073709551615UL
+#define RAND_MAX 32767
+
+/* C99 inttypes.h defines */
+#define PRId8 "d"
+#define PRIi8 "i"
+#define PRIo8 "o"
+#define PRIu8 "u"
+#define PRIx8 "x"
+#define PRIX8 "X"
+#define PRId16 "d"
+#define PRIi16 "i"
+#define PRIo16 "o"
+#define PRIu16 "u"
+#define PRIx16 "x"
+#define PRIX16 "X"
+#define PRId32 "d"
+#define PRIi32 "i"
+#define PRIo32 "o"
+#define PRIu32 "u"
+#define PRIx32 "x"
+#define PRIX32 "X"
+#define PRId64 "d"
+#define PRIi64 "i"
+#define PRIo64 "o"
+#define PRIu64 "u"
+#define PRIx64 "x"
+#define PRIX64 "X"
+#define PRIdLEAST8 "d"
+#define PRIiLEAST8 "i"
+#define PRIoLEAST8 "o"
+#define PRIuLEAST8 "u"
+#define PRIxLEAST8 "x"
+#define PRIXLEAST8 "X"
+#define PRIdLEAST16 "d"
+#define PRIiLEAST16 "i"
+#define PRIoLEAST16 "o"
+#define PRIuLEAST16 "u"
+#define PRIxLEAST16 "x"
+#define PRIXLEAST16 "X"
+#define PRIdLEAST32 "d"
+#define PRIiLEAST32 "i"
+#define PRIoLEAST32 "o"
+#define PRIuLEAST32 "u"
+#define PRIxLEAST32 "x"
+#define PRIXLEAST32 "X"
+#define PRIdLEAST64 "d"
+#define PRIiLEAST64 "i"
+#define PRIoLEAST64 "o"
+#define PRIuLEAST64 "u"
+#define PRIxLEAST64 "x"
+#define PRIXLEAST64 "X"
+#define PRIdFAST8 "d"
+#define PRIiFAST8 "i"
+#define PRIoFAST8 "o"
+#define PRIuFAST8 "u"
+#define PRIxFAST8 "x"
+#define PRIXFAST8 "X"
+#define PRIdFAST16 "d"
+#define PRIiFAST16 "i"
+#define PRIoFAST16 "o"
+#define PRIuFAST16 "u"
+#define PRIxFAST16 "x"
+#define PRIXFAST16 "X"
+#define PRIdFAST32 "d"
+#define PRIiFAST32 "i"
+#define PRIoFAST32 "o"
+#define PRIuFAST32 "u"
+#define PRIxFAST32 "x"
+#define PRIXFAST32 "X"
+#define PRIdFAST64 "d"
+#define PRIiFAST64 "i"
+#define PRIoFAST64 "o"
+#define PRIuFAST64 "u"
+#define PRIxFAST64 "x"
+#define PRIXFAST64 "X"
+#define PRIdPTR "d"
+#define PRIiPTR "i"
+#define PRIoPTR "o"
+#define PRIuPTR "u"
+#define PRIxPTR "x"
+#define PRIXPTR "X"
+#define PRIdMAX "d"
+#define PRIiMAX "i"
+#define PRIoMAX "o"
+#define PRIuMAX "u"
+#define PRIxMAX "x"
+#define PRIXMAX "X"
+#define SCNd8 "d"
+#define SCNi8 "i"
+#define SCNo8 "o"
+#define SCNu8 "u"
+#define SCNx8 "x"
+#define SCNd16 "d"
+#define SCNi16 "i"
+#define SCNo16 "o"
+#define SCNu16 "u"
+#define SCNx16 "x"
+#define SCNd32 "d"
+#define SCNi32 "i"
+#define SCNo32 "o"
+#define SCNu32 "u"
+#define SCNx32 "x"
+#define SCNd64 "d"
+#define SCNi64 "i"
+#define SCNo64 "o"
+#define SCNu64 "u"
+#define SCNx64 "x"
+#define SCNdLEAST8 "d"
+#define SCNiLEAST8 "i"
+#define SCNoLEAST8 "o"
+#define SCNuLEAST8 "u"
+#define SCNxLEAST8 "x"
+#define SCNdLEAST16 "d"
+#define SCNiLEAST16 "i"
+#define SCNoLEAST16 "o"
+#define SCNuLEAST16 "u"
+#define SCNxLEAST16 "x"
+#define SCNdLEAST32 "d"
+#define SCNiLEAST32 "i"
+#define SCNoLEAST32 "o"
+#define SCNuLEAST32 "u"
+#define SCNxLEAST32 "x"
+#define SCNdLEAST64 "d"
+#define SCNiLEAST64 "i"
+#define SCNoLEAST64 "o"
+#define SCNuLEAST64 "u"
+#define SCNxLEAST64 "x"
+#define SCNdFAST8 "d"
+#define SCNiFAST8 "i"
+#define SCNoFAST8 "o"
+#define SCNuFAST8 "u"
+#define SCNxFAST8 "x"
+#define SCNdFAST16 "d"
+#define SCNiFAST16 "i"
+#define SCNoFAST16 "o"
+#define SCNuFAST16 "u"
+#define SCNxFAST16 "x"
+#define SCNdFAST32 "d"
+#define SCNiFAST32 "i"
+#define SCNoFAST32 "o"
+#define SCNuFAST32 "u"
+#define SCNxFAST32 "x"
+#define SCNdFAST64 "d"
+#define SCNiFAST64 "i"
+#define SCNoFAST64 "o"
+#define SCNuFAST64 "u"
+#define SCNxFAST64 "x"
+#define SCNdPTR "d"
+#define SCNiPTR "i"
+#define SCNoPTR "o"
+#define SCNuPTR "u"
+#define SCNxPTR "x"
+#define SCNdMAX "d"
+#define SCNiMAX "i"
+#define SCNoMAX "o"
+#define SCNuMAX "u"
+#define SCNxMAX "x"
+
+/* C99 stdbool.h defines */
+#define __bool_true_false_are_defined 1
+#define false 0
+#define true 1
+
+/* va_arg macros and type*/
+#define va_start(_ap, _type) __builtin_va_start((_ap))
+#define va_arg(_ap, _type) __builtin_va_arg((_ap))
+#define va_end(_list)
+
+/* Vectors */
+#define __m128    int
+#define __m128_u  int
+#define __m128d   int
+#define __m128d_u int
+#define __m128i   int
+#define __m128i_u int
+#define __m256    int
+#define __m256_u  int
+#define __m256d   int
+#define __m256d_u int
+#define __m256i   int
+#define __m256i_u int
+#define __m512    int
+#define __m512_u  int
+#define __m512d   int
+#define __m512d_u int
+#define __m512i   int
+#define __m512i_u int
+
+/* C11 stdnoreturn.h defines */
+#define __noreturn_is_defined 1
+#define noreturn _Noreturn
+
+/* C11 threads.h defines */
+#define thread_local _Thread_local
+
+/* C11 assert.h defines */
+#define static_assert _Static_assert
+
+/* C11 stdatomic.h defines */
+#define ATOMIC_BOOL_LOCK_FREE       0
+#define ATOMIC_CHAR_LOCK_FREE       0
+#define ATOMIC_CHAR16_T_LOCK_FREE   0
+#define ATOMIC_CHAR32_T_LOCK_FREE   0
+#define ATOMIC_WCHAR_T_LOCK_FREE    0
+#define ATOMIC_SHORT_LOCK_FREE      0
+#define ATOMIC_INT_LOCK_FREE        0
+#define ATOMIC_LONG_LOCK_FREE       0
+#define ATOMIC_LLONG_LOCK_FREE      0
+#define ATOMIC_POINTER_LOCK_FREE    0
+#define ATOMIC_VAR_INIT(value) (value)
+#define ATOMIC_FLAG_INIT { 0 }
+#define kill_dependency(y) (y)
+
+/* C11 stdalign.h defines */
+#define alignas _Alignas
+#define alignof _Alignof
+#define __alignas_is_defined 1
+#define __alignof_is_defined 1
+
+#endif
diff --git a/generator/fake_libc_include/_fake_typedefs.h b/generator/fake_libc_include/_fake_typedefs.h
new file mode 100644
index 0000000..3be1462
--- /dev/null
+++ b/generator/fake_libc_include/_fake_typedefs.h
@@ -0,0 +1,222 @@
+#ifndef _FAKE_TYPEDEFS_H
+#define _FAKE_TYPEDEFS_H
+
+typedef int size_t;
+typedef int __builtin_va_list;
+typedef int __gnuc_va_list;
+typedef int va_list;
+typedef int __int8_t;
+typedef int __uint8_t;
+typedef int __int16_t;
+typedef int __uint16_t;
+typedef int __int_least16_t;
+typedef int __uint_least16_t;
+typedef int __int32_t;
+typedef int __uint32_t;
+typedef int __int64_t;
+typedef int __uint64_t;
+typedef int __int_least32_t;
+typedef int __uint_least32_t;
+typedef int __s8;
+typedef int __u8;
+typedef int __s16;
+typedef int __u16;
+typedef int __s32;
+typedef int __u32;
+typedef int __s64;
+typedef int __u64;
+typedef int _LOCK_T;
+typedef int _LOCK_RECURSIVE_T;
+typedef int _off_t;
+typedef int __dev_t;
+typedef int __uid_t;
+typedef int __gid_t;
+typedef int _off64_t;
+typedef int _fpos_t;
+typedef int _ssize_t;
+typedef int wint_t;
+typedef int _mbstate_t;
+typedef int _flock_t;
+typedef int _iconv_t;
+typedef int __ULong;
+typedef int __FILE;
+typedef int ptrdiff_t;
+typedef int wchar_t;
+typedef int char16_t;
+typedef int char32_t;
+typedef int __off_t;
+typedef int __pid_t;
+typedef int __loff_t;
+typedef int u_char;
+typedef int u_short;
+typedef int u_int;
+typedef int u_long;
+typedef int ushort;
+typedef int uint;
+typedef int clock_t;
+typedef int time_t;
+typedef int daddr_t;
+typedef int caddr_t;
+typedef int ino_t;
+typedef int off_t;
+typedef int dev_t;
+typedef int uid_t;
+typedef int gid_t;
+typedef int pid_t;
+typedef int key_t;
+typedef int ssize_t;
+typedef int mode_t;
+typedef int nlink_t;
+typedef int fd_mask;
+typedef int _types_fd_set;
+typedef int clockid_t;
+typedef int timer_t;
+typedef int useconds_t;
+typedef int suseconds_t;
+typedef int FILE;
+typedef int fpos_t;
+typedef int cookie_read_function_t;
+typedef int cookie_write_function_t;
+typedef int cookie_seek_function_t;
+typedef int cookie_close_function_t;
+typedef int cookie_io_functions_t;
+typedef int div_t;
+typedef int ldiv_t;
+typedef int lldiv_t;
+typedef int sigset_t;
+typedef int __sigset_t;
+typedef int _sig_func_ptr;
+typedef int sig_atomic_t;
+typedef int __tzrule_type;
+typedef int __tzinfo_type;
+typedef int mbstate_t;
+typedef int sem_t;
+typedef int pthread_t;
+typedef int pthread_attr_t;
+typedef int pthread_mutex_t;
+typedef int pthread_mutexattr_t;
+typedef int pthread_cond_t;
+typedef int pthread_condattr_t;
+typedef int pthread_key_t;
+typedef int pthread_once_t;
+typedef int pthread_rwlock_t;
+typedef int pthread_rwlockattr_t;
+typedef int pthread_spinlock_t;
+typedef int pthread_barrier_t;
+typedef int pthread_barrierattr_t;
+typedef int jmp_buf;
+typedef int rlim_t;
+typedef int sa_family_t;
+typedef int sigjmp_buf;
+typedef int stack_t;
+typedef int siginfo_t;
+typedef int z_stream;
+
+/* C99 exact-width integer types */
+typedef int int8_t;
+typedef int uint8_t;
+typedef int int16_t;
+typedef int uint16_t;
+typedef int int32_t;
+typedef int uint32_t;
+typedef int int64_t;
+typedef int uint64_t;
+
+/* C99 minimum-width integer types */
+typedef int int_least8_t;
+typedef int uint_least8_t;
+typedef int int_least16_t;
+typedef int uint_least16_t;
+typedef int int_least32_t;
+typedef int uint_least32_t;
+typedef int int_least64_t;
+typedef int uint_least64_t;
+
+/* C99 fastest minimum-width integer types */
+typedef int int_fast8_t;
+typedef int uint_fast8_t;
+typedef int int_fast16_t;
+typedef int uint_fast16_t;
+typedef int int_fast32_t;
+typedef int uint_fast32_t;
+typedef int int_fast64_t;
+typedef int uint_fast64_t;
+
+/* C99 integer types capable of holding object pointers */
+typedef int intptr_t;
+typedef int uintptr_t;
+
+/* C99 greatest-width integer types */
+typedef int intmax_t;
+typedef int uintmax_t;
+
+/* C99 stdbool.h bool type. _Bool is built-in in C99 */
+typedef _Bool bool;
+
+/* Mir typedefs */
+typedef void* MirEGLNativeWindowType;
+typedef void* MirEGLNativeDisplayType;
+typedef struct MirConnection MirConnection;
+typedef struct MirSurface MirSurface;
+typedef struct MirSurfaceSpec MirSurfaceSpec;
+typedef struct MirScreencast MirScreencast;
+typedef struct MirPromptSession MirPromptSession;
+typedef struct MirBufferStream MirBufferStream;
+typedef struct MirPersistentId MirPersistentId;
+typedef struct MirBlob MirBlob;
+typedef struct MirDisplayConfig MirDisplayConfig;
+
+/* xcb typedefs */
+typedef struct xcb_connection_t xcb_connection_t;
+typedef uint32_t xcb_window_t;
+typedef uint32_t xcb_visualid_t;
+
+/* C11 stdatomic.h types */
+typedef _Atomic(_Bool)              atomic_bool;
+typedef _Atomic(char)               atomic_char;
+typedef _Atomic(signed char)        atomic_schar;
+typedef _Atomic(unsigned char)      atomic_uchar;
+typedef _Atomic(short)              atomic_short;
+typedef _Atomic(unsigned short)     atomic_ushort;
+typedef _Atomic(int)                atomic_int;
+typedef _Atomic(unsigned int)       atomic_uint;
+typedef _Atomic(long)               atomic_long;
+typedef _Atomic(unsigned long)      atomic_ulong;
+typedef _Atomic(long long)          atomic_llong;
+typedef _Atomic(unsigned long long) atomic_ullong;
+typedef _Atomic(uint_least16_t)     atomic_char16_t;
+typedef _Atomic(uint_least32_t)     atomic_char32_t;
+typedef _Atomic(wchar_t)            atomic_wchar_t;
+typedef _Atomic(int_least8_t)       atomic_int_least8_t;
+typedef _Atomic(uint_least8_t)      atomic_uint_least8_t;
+typedef _Atomic(int_least16_t)      atomic_int_least16_t;
+typedef _Atomic(uint_least16_t)     atomic_uint_least16_t;
+typedef _Atomic(int_least32_t)      atomic_int_least32_t;
+typedef _Atomic(uint_least32_t)     atomic_uint_least32_t;
+typedef _Atomic(int_least64_t)      atomic_int_least64_t;
+typedef _Atomic(uint_least64_t)     atomic_uint_least64_t;
+typedef _Atomic(int_fast8_t)        atomic_int_fast8_t;
+typedef _Atomic(uint_fast8_t)       atomic_uint_fast8_t;
+typedef _Atomic(int_fast16_t)       atomic_int_fast16_t;
+typedef _Atomic(uint_fast16_t)      atomic_uint_fast16_t;
+typedef _Atomic(int_fast32_t)       atomic_int_fast32_t;
+typedef _Atomic(uint_fast32_t)      atomic_uint_fast32_t;
+typedef _Atomic(int_fast64_t)       atomic_int_fast64_t;
+typedef _Atomic(uint_fast64_t)      atomic_uint_fast64_t;
+typedef _Atomic(intptr_t)           atomic_intptr_t;
+typedef _Atomic(uintptr_t)          atomic_uintptr_t;
+typedef _Atomic(size_t)             atomic_size_t;
+typedef _Atomic(ptrdiff_t)          atomic_ptrdiff_t;
+typedef _Atomic(intmax_t)           atomic_intmax_t;
+typedef _Atomic(uintmax_t)          atomic_uintmax_t;
+typedef struct atomic_flag { atomic_bool _Value; } atomic_flag;
+typedef enum memory_order {
+  memory_order_relaxed,
+  memory_order_consume,
+  memory_order_acquire,
+  memory_order_release,
+  memory_order_acq_rel,
+  memory_order_seq_cst
+} memory_order;
+
+#endif
diff --git a/generator/fake_libc_include/_syslist.h b/generator/fake_libc_include/_syslist.h
new file mode 100644
index 0000000..f952c1d
--- /dev/null
+++ b/generator/fake_libc_include/_syslist.h
@@ -0,0 +1,2 @@
+#include "_fake_defines.h"
+#include "_fake_typedefs.h"
diff --git a/generator/fake_libc_include/stdbool.h b/generator/fake_libc_include/stdbool.h
new file mode 100644
index 0000000..f952c1d
--- /dev/null
+++ b/generator/fake_libc_include/stdbool.h
@@ -0,0 +1,2 @@
+#include "_fake_defines.h"
+#include "_fake_typedefs.h"
diff --git a/generator/fake_libc_include/stddef.h b/generator/fake_libc_include/stddef.h
new file mode 100644
index 0000000..f952c1d
--- /dev/null
+++ b/generator/fake_libc_include/stddef.h
@@ -0,0 +1,2 @@
+#include "_fake_defines.h"
+#include "_fake_typedefs.h"
diff --git a/generator/fake_libc_include/stdint.h b/generator/fake_libc_include/stdint.h
new file mode 100644
index 0000000..f952c1d
--- /dev/null
+++ b/generator/fake_libc_include/stdint.h
@@ -0,0 +1,2 @@
+#include "_fake_defines.h"
+#include "_fake_typedefs.h"
diff --git a/generator/gen_repo_bindings.sh b/generator/gen_repo_bindings.sh
new file mode 100644
index 0000000..067c753
--- /dev/null
+++ b/generator/gen_repo_bindings.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/bash
+REPO_ROOT=$(realpath $(dirname $(dirname $0)))
+
+# Generate odin bindings
+python $REPO_ROOT/generator/cli.py $REPO_ROOT/clay.h --output-dir $REPO_ROOT/bindings/odin/clay-odin --generator odin --verbose
\ No newline at end of file
diff --git a/generator/generators/__init__.py b/generator/generators/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/generator/generators/base_generator.py b/generator/generators/base_generator.py
new file mode 100644
index 0000000..caca0cc
--- /dev/null
+++ b/generator/generators/base_generator.py
@@ -0,0 +1,47 @@
+
+from parser import ExtractedSymbols, ExtractedEnum, ExtractedStruct, ExtractedFunction
+from typing import Any, Callable, DefaultDict, Literal, NotRequired, Optional, TypedDict
+from pathlib import Path
+from dataclasses import dataclass
+
+SymbolType = Literal['enum', 'struct', 'function']
+
+class BaseGenerator:
+    def __init__(self, extracted_symbols: ExtractedSymbols):
+        self.extracted_symbols = extracted_symbols
+        self.output_content: dict[str, list[str]] = dict()
+
+    def generate(self) -> None:
+        pass
+
+    def has_symbol(self, symbol: str) -> bool:
+        return (
+            symbol in self.extracted_symbols.enums or 
+            symbol in self.extracted_symbols.structs or 
+            symbol in self.extracted_symbols.functions
+        )
+
+    def get_symbol_type(self, symbol: str) -> SymbolType:
+        if symbol in self.extracted_symbols.enums:
+            return 'enum'
+        elif symbol in self.extracted_symbols.structs:
+            return 'struct'
+        elif symbol in self.extracted_symbols.functions:
+            return 'function'
+        raise ValueError(f'Unknown symbol: {symbol}')
+
+    def _write(self, file_name: str, content: str) -> None:
+        if file_name not in self.output_content:
+            self.output_content[file_name] = []
+        self.output_content[file_name].append(content)
+
+    def write_outputs(self, output_dir: Path) -> None:
+        for file_name, content in self.output_content.items():
+            (output_dir / file_name).parent.mkdir(parents=True, exist_ok=True)
+            with open(output_dir / file_name, 'w') as f:
+                f.write("\n".join(content))
+
+    def get_outputs(self) -> dict[str, str]:
+        return {file_name: "\n".join(content) for file_name, content in self.output_content.items()}
+
+
diff --git a/generator/generators/odin/clay.template.odin b/generator/generators/odin/clay.template.odin
new file mode 100644
index 0000000..0b0bfad
--- /dev/null
+++ b/generator/generators/odin/clay.template.odin
@@ -0,0 +1,164 @@
+package clay
+
+import "core:c"
+import "core:strings"
+
+when ODIN_OS == .Windows {
+    foreign import Clay "windows/clay.lib"
+} else when ODIN_OS == .Linux {
+    foreign import Clay "linux/clay.a"
+} else when ODIN_OS == .Darwin {
+    when ODIN_ARCH == .arm64 {
+        foreign import Clay "macos-arm64/clay.a"
+    } else {
+        foreign import Clay "macos/clay.a"
+    }
+} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
+    foreign import Clay "wasm/clay.o"
+}
+
+when ODIN_OS == .Windows {
+    EnumBackingType :: u32
+} else {
+    EnumBackingType :: u8
+}
+
+{{enums}}
+
+Context :: struct {
+    
+}
+
+ClayArray :: struct($type: typeid) {
+    capacity:      i32,
+    length:        i32,
+    internalArray: [^]type,
+}
+
+SizingConstraints :: struct #raw_union {
+    sizeMinMax:  SizingConstraintsMinMax,
+    sizePercent: c.float,
+}
+
+TypedConfig :: struct {
+    type:   ElementConfigType,
+    config: rawptr,
+    id:     ElementId,
+}
+
+{{structs}}
+
+@(link_prefix = "Clay_", default_calling_convention = "c")
+foreign Clay {
+{{public_functions}}
+}
+
+@(link_prefix = "Clay_", default_calling_convention = "c", private)
+foreign Clay {
+{{private_functions}}
+}
+
+@(require_results, deferred_none = _CloseElement)
+UI :: proc(configs: ..TypedConfig) -> bool {
+    _OpenElement()
+    for config in configs {
+        #partial switch (config.type) {
+        case ElementConfigType.Id:
+            _AttachId(config.id)
+        case ElementConfigType.Layout:
+            _AttachLayoutConfig(cast(^LayoutConfig)config.config)
+        case:
+            _AttachElementConfig(config.config, config.type)
+        }
+    }
+    _ElementPostConfiguration()
+    return true
+}
+
+Layout :: proc(config: LayoutConfig) -> TypedConfig {
+    return {type = ElementConfigType.Layout, config = _StoreLayoutConfig(config) }
+}
+
+PaddingAll :: proc (padding: u16) -> Padding {
+    return { padding, padding, padding, padding }
+}
+
+Rectangle :: proc(config: RectangleElementConfig) -> TypedConfig {
+    return {type = ElementConfigType.Rectangle, config = _StoreRectangleElementConfig(config)}
+}
+
+Text :: proc(text: string, config: ^TextElementConfig) {
+    _OpenTextElement(MakeString(text), config)
+}
+
+TextConfig :: proc(config: TextElementConfig) -> ^TextElementConfig {
+    return _StoreTextElementConfig(config)
+}
+
+Image :: proc(config: ImageElementConfig) -> TypedConfig {
+    return {type = ElementConfigType.Image, config = _StoreImageElementConfig(config)}
+}
+
+Floating :: proc(config: FloatingElementConfig) -> TypedConfig {
+    return {type = ElementConfigType.Floating, config = _StoreFloatingElementConfig(config)}
+}
+
+Custom :: proc(config: CustomElementConfig) -> TypedConfig {
+    return {type = ElementConfigType.Custom, config = _StoreCustomElementConfig(config)}
+}
+
+Scroll :: proc(config: ScrollElementConfig) -> TypedConfig {
+    return {type = ElementConfigType.Scroll, config = _StoreScrollElementConfig(config)}
+}
+
+Border :: proc(config: BorderElementConfig) -> TypedConfig {
+    return {type = ElementConfigType.Border, config = _StoreBorderElementConfig(config)}
+}
+
+BorderOutside :: proc(outsideBorders: BorderData) -> TypedConfig {
+    return { type = ElementConfigType.Border, config = _StoreBorderElementConfig((BorderElementConfig){left = outsideBorders, right = outsideBorders, top = outsideBorders, bottom = outsideBorders}) }
+}
+
+BorderOutsideRadius :: proc(outsideBorders: BorderData, radius: f32) -> TypedConfig {
+    return { type = ElementConfigType.Border, config = _StoreBorderElementConfig(
+        (BorderElementConfig){left = outsideBorders, right = outsideBorders, top = outsideBorders, bottom = outsideBorders, cornerRadius = {radius, radius, radius, radius}},
+    ) }
+}
+
+BorderAll :: proc(allBorders: BorderData) -> TypedConfig {
+    return { type = ElementConfigType.Border, config = _StoreBorderElementConfig((BorderElementConfig){left = allBorders, right = allBorders, top = allBorders, bottom = allBorders, betweenChildren = allBorders}) }
+}
+
+BorderAllRadius :: proc(allBorders: BorderData, radius: f32) -> TypedConfig {
+    return { type = ElementConfigType.Border, config = _StoreBorderElementConfig(
+        (BorderElementConfig){left = allBorders, right = allBorders, top = allBorders, bottom = allBorders, cornerRadius = {radius, radius, radius, radius}},
+    ) }
+}
+
+CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
+    return CornerRadius{radius, radius, radius, radius}
+}
+
+SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
+    return SizingAxis{type = SizingType.FIT, constraints = {sizeMinMax = sizeMinMax}}
+}
+
+SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
+    return SizingAxis{type = SizingType.GROW, constraints = {sizeMinMax = sizeMinMax}}
+}
+
+SizingFixed :: proc(size: c.float) -> SizingAxis {
+    return SizingAxis{type = SizingType.FIXED, constraints = {sizeMinMax = {size, size}}}
+}
+
+SizingPercent :: proc(sizePercent: c.float) -> SizingAxis {
+    return SizingAxis{type = SizingType.PERCENT, constraints = {sizePercent = sizePercent}}
+}
+
+MakeString :: proc(label: string) -> String {
+    return String{chars = raw_data(label), length = cast(c.int)len(label)}
+}
+
+ID :: proc(label: string, index: u32 = 0) -> TypedConfig {
+    return { type = ElementConfigType.Id, id = _HashString(MakeString(label), index, 0) }
+}
diff --git a/generator/generators/odin_generator.py b/generator/generators/odin_generator.py
new file mode 100644
index 0000000..75db16a
--- /dev/null
+++ b/generator/generators/odin_generator.py
@@ -0,0 +1,315 @@
+from pathlib import Path
+import logging
+
+from parser import ExtractedSymbolType
+from generators.base_generator import BaseGenerator
+
+logger = logging.getLogger(__name__)
+
+def get_common_prefix(keys: list[str]) -> str:
+    # find a prefix that's shared between all keys
+    prefix = ""
+    for i in range(min(map(len, keys))):
+        if len(set(key[i] for key in keys)) > 1:
+            break
+        prefix += keys[0][i]
+    return prefix
+
+def snake_case_to_pascal_case(snake_case: str) -> str:
+    return ''.join(word.lower().capitalize() for word in snake_case.split('_'))
+
+
+SYMBOL_NAME_OVERRIDES = {
+    'Clay_TextElementConfigWrapMode': 'TextWrapMode',
+    'Clay_Border': 'BorderData',
+    'Clay_SizingMinMax': 'SizingConstraintsMinMax',
+}
+SYMBOL_COMPLETE_OVERRIDES = {
+    'Clay_RenderCommandArray': 'ClayArray(RenderCommand)',
+    'Clay_Context': 'Context',
+    'Clay_ElementConfig': None,
+    # 'Clay_SetQueryScrollOffsetFunction': None,
+}
+
+# These enums should have output binding members that are PascalCase instead of UPPER_SNAKE_CASE.
+ENUM_MEMBER_PASCAL = {
+    'Clay_RenderCommandType',
+    'Clay_TextElementConfigWrapMode',
+    'Clay__ElementConfigType',
+}
+ENUM_MEMBER_OVERRIDES = {
+    'Clay__ElementConfigType': {
+        'CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER': 'Border',
+        'CLAY__ELEMENT_CONFIG_TYPE_FLOATING_CONTAINER': 'Floating',
+        'CLAY__ELEMENT_CONFIG_TYPE_SCROLL_CONTAINER': 'Scroll',
+    }
+}
+ENUM_ADDITIONAL_MEMBERS = {
+    'Clay__ElementConfigType': {
+        'Id': 65,
+        'Layout': 66,
+    }
+}
+
+TYPE_MAPPING = {
+    '*char': '[^]c.char',
+    'const *char': '[^]c.char',
+    '*void': 'rawptr',
+    'bool': 'bool',
+    'float': 'c.float',
+    'uint16_t': 'u16',
+    'uint32_t': 'u32',
+    'int32_t': 'c.int32_t',
+    'uintptr_t': 'rawptr',
+    'intptr_t': 'rawptr',
+    'void': 'void',
+}
+STRUCT_TYPE_OVERRIDES = {
+    'Clay_Arena': {
+        'nextAllocation': 'uintptr',
+        'capacity': 'uintptr',
+    },
+    'Clay_SizingAxis': {
+        'size': 'SizingConstraints',
+    },
+    "Clay_RenderCommand": {
+        "zIndex": 'i32',
+    },
+}
+STRUCT_MEMBER_OVERRIDES = {
+    'Clay_ErrorHandler': {
+        'errorHandlerFunction': 'handler',
+    },
+    'Clay_SizingAxis': {
+        'size': 'constraints',
+    },
+}
+STRUCT_OVERRIDE_AS_FIXED_ARRAY = {
+    'Clay_Color',
+    'Clay_Vector2',
+}
+
+FUNCTION_PARAM_OVERRIDES = {
+    'Clay_SetCurrentContext': {
+        'context': 'ctx',
+    },
+}
+FUNCTION_TYPE_OVERRIDES = {
+    'Clay_CreateArenaWithCapacityAndMemory': {
+        'offset': '[^]u8',
+    },
+    'Clay_SetMeasureTextFunction': {
+        'userData': 'uintptr',
+    },
+    'Clay_RenderCommandArray_Get': {
+        'index': 'i32',
+    },
+    "Clay__AttachElementConfig": {
+        "config": 'rawptr',
+    },
+}
+
+class OdinGenerator(BaseGenerator):
+
+    def generate(self) -> None:
+        self.generate_structs()
+        self.generate_enums()
+        self.generate_functions()
+
+        odin_template_path = Path(__file__).parent / 'odin' / 'clay.template.odin'
+        with open(odin_template_path, 'r') as f:
+            template = f.read()
+        self.output_content['clay.odin'] = (
+            template
+            .replace('{{structs}}', '\n'.join(self.output_content['struct']))
+            .replace('{{enums}}', '\n'.join(self.output_content['enum']))
+            .replace('{{public_functions}}', '\n'.join(self.output_content['public_function']))
+            .replace('{{private_functions}}', '\n'.join(self.output_content['private_function']))
+            .splitlines()
+        )
+        del self.output_content['struct']
+        del self.output_content['enum']
+        del self.output_content['private_function']
+        del self.output_content['public_function']
+
+    def get_symbol_name(self, symbol: str) -> str:
+        if symbol in SYMBOL_NAME_OVERRIDES:
+            return SYMBOL_NAME_OVERRIDES[symbol]
+        symbol_type = self.get_symbol_type(symbol)
+        base_name = symbol.removeprefix('Clay_')
+        if symbol_type == 'enum':
+            return base_name.removeprefix('_') # Clay_ and Clay__ are exported as public types.
+        elif symbol_type == 'struct':
+            return base_name
+        elif symbol_type == 'function':
+            return base_name
+        raise ValueError(f'Unknown symbol: {symbol}')
+    
+    def format_type(self, type: ExtractedSymbolType) -> str:
+        if isinstance(type, str):
+            return type
+        
+        parameter_strs = []
+        for param_name, param_type in type['params']:
+            parameter_strs.append(f"{param_name}: {self.format_type(param_type or 'unknown')}")
+        return_type_str = ''
+        if type['return_type'] is not None and type['return_type'] != 'void':
+            return_type_str = ' -> ' + self.format_type(type['return_type'])
+        return f"proc \"c\" ({', '.join(parameter_strs)}){return_type_str}"
+    
+    def resolve_binding_type(self, symbol: str, member: str | None, member_type: ExtractedSymbolType | None, type_overrides: dict[str, dict[str, str]]) -> str | None:
+        if isinstance(member_type, str):
+            if member_type in SYMBOL_COMPLETE_OVERRIDES:
+                return SYMBOL_COMPLETE_OVERRIDES[member_type]
+            if symbol in type_overrides and member in type_overrides[symbol]:
+                return type_overrides[symbol][member]
+            if member_type in TYPE_MAPPING:
+                return TYPE_MAPPING[member_type]
+            if member_type and self.has_symbol(member_type):
+                return self.get_symbol_name(member_type)
+            if member_type and member_type.startswith('*'):
+                result = self.resolve_binding_type(symbol, member, member_type[1:], type_overrides)
+                if result:
+                    return f"^{result}"
+            return None
+        if member_type is None:
+            return None
+
+        resolved_parameters = []
+        for param_name, param_type in member_type['params']:
+            resolved_param = self.resolve_binding_type(symbol, param_name, param_type, type_overrides)
+            if resolved_param is None:
+                return None
+            resolved_parameters.append((param_name, resolved_param))
+        resolved_return_type = self.resolve_binding_type(symbol, None, member_type['return_type'], type_overrides)
+        if resolved_return_type is None:
+            return None
+        return self.format_type({
+            "params": resolved_parameters,
+            "return_type": resolved_return_type,
+        })
+
+    def generate_structs(self) -> None:
+        for struct, struct_data in sorted(self.extracted_symbols.structs.items(), key=lambda x: x[0]):
+            members = struct_data['attrs']
+            if not struct.startswith('Clay_'):
+                continue
+            if struct in SYMBOL_COMPLETE_OVERRIDES:
+                continue
+
+            binding_name = self.get_symbol_name(struct)
+            if binding_name.startswith('_'):
+                continue
+
+            if struct in STRUCT_OVERRIDE_AS_FIXED_ARRAY:
+                array_size = len(members)
+                first_elem = list(members.values())[0]
+                array_type = None
+                if 'type' in first_elem:
+                   array_type = first_elem['type']
+
+                if array_type in TYPE_MAPPING:
+                    array_binding_type = TYPE_MAPPING[array_type]
+                elif array_type and self.has_symbol(self.format_type(array_type)):
+                    array_binding_type = self.get_symbol_name(self.format_type(array_type))
+                else:
+                    self._write('struct', f"// {struct} ({array_type}) - has no mapping")
+                    continue
+                
+                self._write('struct', f"// {struct} (overridden as fixed array)")
+                self._write('struct', f"{binding_name} :: [{array_size}]{array_binding_type}")
+                self._write('struct', "")
+                continue
+
+            raw_union = ' #raw_union' if struct_data.get('is_union', False) else ''
+
+            self._write('struct', f"// {struct}")
+            self._write('struct', f"{binding_name} :: struct{raw_union} {{")
+
+            for member, member_info in members.items():
+                if struct in STRUCT_TYPE_OVERRIDES and member in STRUCT_TYPE_OVERRIDES[struct]:
+                    member_type = 'unknown'
+                elif not 'type' in member_info:
+                    self._write('struct', f"    // {member} (unknown type)")
+                    continue
+                else:
+                    member_type = member_info['type']
+                    
+                binding_member_name = member
+                if struct in STRUCT_MEMBER_OVERRIDES and member in STRUCT_MEMBER_OVERRIDES[struct]:
+                    binding_member_name = STRUCT_MEMBER_OVERRIDES[struct][member]
+
+                member_binding_type = self.resolve_binding_type(struct, member, member_type, STRUCT_TYPE_OVERRIDES)
+                if member_binding_type is None:
+                    self._write('struct', f"    // {binding_member_name} ({member_type}) - has no mapping")
+                    continue
+                self._write('struct', f"    {binding_member_name}: {member_binding_type}, // {member} ({member_type})")
+            self._write('struct', "}")
+            self._write('struct', '')
+
+    def generate_enums(self) -> None:
+        for enum, members in sorted(self.extracted_symbols.enums.items(), key=lambda x: x[0]):
+            if not enum.startswith('Clay_'):
+                continue
+            if enum in SYMBOL_COMPLETE_OVERRIDES:
+                continue
+
+            binding_name = self.get_symbol_name(enum)
+            common_member_prefix = get_common_prefix(list(members.keys()))
+            self._write('enum', f"// {enum}")
+            self._write('enum', f"{binding_name} :: enum EnumBackingType {{")
+            for member in members:
+                if enum in ENUM_MEMBER_OVERRIDES and member in ENUM_MEMBER_OVERRIDES[enum]:
+                    binding_member_name = ENUM_MEMBER_OVERRIDES[enum][member]
+                else:
+                    binding_member_name = member.removeprefix(common_member_prefix)
+                    if enum in ENUM_MEMBER_PASCAL:
+                        binding_member_name = snake_case_to_pascal_case(binding_member_name)
+
+                if members[member] is not None:
+                    self._write('enum', f"    {binding_member_name} = {members[member]}, // {member}")
+                else:
+                    self._write('enum', f"    {binding_member_name}, // {member}")
+                
+            if enum in ENUM_ADDITIONAL_MEMBERS:
+                self._write('enum', '    // Odin specific enum types')
+                for member, value in ENUM_ADDITIONAL_MEMBERS[enum].items():
+                    self._write('enum', f"    {member} = {value},")
+            self._write('enum', "}")
+            self._write('enum', '')
+
+    def generate_functions(self) -> None:
+        for function, function_info in sorted(self.extracted_symbols.functions.items(), key=lambda x: x[0]):
+            if not function.startswith('Clay_'):
+                continue
+            if function in SYMBOL_COMPLETE_OVERRIDES:
+                continue
+            is_private = function.startswith('Clay__')
+            write_to = 'private_function' if is_private else 'public_function'
+
+            binding_name = self.get_symbol_name(function)
+
+            return_type = function_info['return_type']
+            binding_return_type = self.resolve_binding_type(function, None, return_type, {})
+            if binding_return_type is None:
+                self._write(write_to, f"    // {function} ({return_type}) - has no mapping")
+                continue
+
+            skip = False
+            binding_params = []
+            for param_name, param_type in function_info['params']:
+                binding_param_name = param_name
+                if function in FUNCTION_PARAM_OVERRIDES and param_name in FUNCTION_PARAM_OVERRIDES[function]:
+                    binding_param_name = FUNCTION_PARAM_OVERRIDES[function][param_name]
+                binding_param_type = self.resolve_binding_type(function, param_name, param_type, FUNCTION_TYPE_OVERRIDES)
+                if binding_param_type is None:
+                    skip = True
+                binding_params.append(f"{binding_param_name}: {binding_param_type}")
+            if skip:
+                self._write(write_to, f"    // {function} - has no mapping")
+                continue
+
+            binding_params_str = ', '.join(binding_params)
+            return_str = f" -> {binding_return_type}" if binding_return_type != 'void' else ''
+            self._write(write_to, f"    {binding_name} :: proc({binding_params_str}){return_str} --- // {function}")
+            
diff --git a/generator/parser.py b/generator/parser.py
new file mode 100644
index 0000000..f652e49
--- /dev/null
+++ b/generator/parser.py
@@ -0,0 +1,173 @@
+from dataclasses import dataclass
+from typing import Optional, TypedDict, NotRequired, Union
+from pycparser import c_ast, parse_file, preprocess_file
+from pathlib import Path
+import os
+import json
+import shutil
+import logging
+
+logger = logging.getLogger(__name__)
+
+ExtractedSymbolType = Union[str, "ExtractedFunction"]
+
+class ExtractedStructAttributeUnion(TypedDict):
+    type: Optional[ExtractedSymbolType]
+
+class ExtractedStructAttribute(TypedDict):
+    type: NotRequired[ExtractedSymbolType]
+    union: NotRequired[dict[str, Optional[ExtractedSymbolType]]]
+
+class ExtractedStruct(TypedDict):
+    attrs: dict[str, ExtractedStructAttribute]
+    is_union: NotRequired[bool]
+
+ExtractedEnum = dict[str, Optional[str]]
+ExtractedFunctionParam = tuple[str, Optional[ExtractedSymbolType]]
+
+class ExtractedFunction(TypedDict):
+    return_type: Optional["ExtractedSymbolType"]
+    params: list[ExtractedFunctionParam]
+
+@dataclass
+class ExtractedSymbols:
+    structs: dict[str, ExtractedStruct]
+    enums: dict[str, ExtractedEnum]
+    functions: dict[str, ExtractedFunction]
+
+def get_type_names(node: c_ast.Node, prefix: str="") -> Optional[ExtractedSymbolType]:
+    if isinstance(node, c_ast.TypeDecl) and hasattr(node, 'quals') and node.quals:
+        prefix = " ".join(node.quals) + " " + prefix
+    if isinstance(node, c_ast.PtrDecl):
+        prefix = "*" + prefix
+    if isinstance(node, c_ast.FuncDecl):
+        func: ExtractedFunction = {
+            'return_type': get_type_names(node.type),
+            'params': [],
+        }
+        for param in node.args.params:
+            if param.name is None:
+                continue
+            func['params'].append((param.name, get_type_names(param)))
+        return func
+
+    if hasattr(node, 'names'):
+        return prefix + node.names[0] # type: ignore
+    elif hasattr(node, 'type'):
+        return get_type_names(node.type, prefix) # type: ignore
+    return None
+
+class Visitor(c_ast.NodeVisitor):
+    def __init__(self):
+        self.structs: dict[str, ExtractedStruct] = {}
+        self.enums: dict[str, ExtractedEnum] = {}
+        self.functions: dict[str, ExtractedFunction] = {}
+
+    def visit_FuncDecl(self, node: c_ast.FuncDecl):
+        # node.show()
+        # logger.debug(node)
+        node_type = node.type
+        is_pointer = False
+        if isinstance(node.type, c_ast.PtrDecl):
+            node_type = node.type.type
+            is_pointer = True
+        
+        if hasattr(node_type, "declname"):
+            return_type = get_type_names(node_type.type)
+            if return_type is not None and isinstance(return_type, str) and is_pointer:
+                return_type = "*" + return_type
+            func: ExtractedFunction = {
+                'return_type': return_type,
+                'params': [],
+            }
+            for param in node.args.params:
+                if param.name is None:
+                    continue
+                func['params'].append((param.name, get_type_names(param)))
+            self.functions[node_type.declname] = func
+        self.generic_visit(node)
+
+    def visit_Struct(self, node: c_ast.Struct):
+        # node.show()
+        if node.name and node.decls:
+            struct = {}
+            for decl in node.decls:
+                struct[decl.name] = {
+                    "type": get_type_names(decl),
+                }
+            self.structs[node.name] = {
+                'attrs': struct,
+            }
+        self.generic_visit(node)
+
+    def visit_Typedef(self, node: c_ast.Typedef):
+        # node.show()
+        if hasattr(node.type, 'type') and hasattr(node.type.type, 'decls') and node.type.type.decls:
+            struct = {}
+            for decl in node.type.type.decls:
+                if hasattr(decl, 'type') and hasattr(decl.type, 'type') and isinstance(decl.type.type, c_ast.Union):
+                    union = {}
+                    for field in decl.type.type.decls:
+                        union[field.name] = get_type_names(field)
+                    struct[decl.name] = {
+                        'union': union
+                    }
+                else:
+                    struct[decl.name] = {
+                        "type": get_type_names(decl),
+                    }
+
+            self.structs[node.name] = {
+                'attrs': struct,
+                'is_union': isinstance(node.type.type, c_ast.Union),
+            }
+        if hasattr(node.type, 'type') and isinstance(node.type.type, c_ast.Enum):
+            enum = {}
+            for enumerator in node.type.type.values.enumerators:
+                if enumerator.value is None:
+                    enum[enumerator.name] = None
+                else:
+                    enum[enumerator.name] = enumerator.value.value
+            self.enums[node.name] = enum
+        self.generic_visit(node)
+
+
+def parse_headers(input_files: list[Path], tmp_dir: Path) -> ExtractedSymbols:
+    cpp_args = ["-nostdinc", "-D__attribute__(x)=", "-E"]
+
+    # Make a new clay.h that combines the provided input files, so that we can add bindings for customized structs
+    with open(tmp_dir / 'merged_clay.h', 'w') as f:
+        for input_file in input_files:
+            with open(input_file, 'r') as f2:
+                for line in f2:
+                    # Ignore includes, as they should be manually included in input_files.
+                    if line.startswith("#include"):
+                        continue
+
+                    # Ignore the CLAY_IMPLEMENTATION define, because we only want to parse the public api code.
+                    # This is helpful so that the user can provide their implementation code, which will contain any custom extensions
+                    if "#define CLAY_IMPLEMENTATION" in line:
+                        continue
+
+                    f.write(line)
+
+    # Preprocess the file
+    logger.info("Preprocessing file")
+    preprocessed = preprocess_file(tmp_dir / 'merged_clay.h', cpp_path="cpp", cpp_args=cpp_args) # type: ignore
+    with open(tmp_dir / 'clay.preprocessed.h', 'w') as f:
+        f.write(preprocessed)
+
+    # Parse the file
+    logger.info("Parsing file")
+    ast = parse_file(tmp_dir / 'clay.preprocessed.h', use_cpp=False) # type: ignore
+
+    # Extract symbols
+    visitor = Visitor()
+    visitor.visit(ast)
+    
+    result = ExtractedSymbols(
+        structs=visitor.structs,
+        enums=visitor.enums,
+        functions=visitor.functions
+    )
+    return result
\ No newline at end of file
diff --git a/generator/requirements.txt b/generator/requirements.txt
new file mode 100644
index 0000000..64dea8a
--- /dev/null
+++ b/generator/requirements.txt
@@ -0,0 +1 @@
+pycparser==2.22
\ No newline at end of file