Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
SortIncludes: true
18 changes: 16 additions & 2 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,23 @@ Checks: >
bugprone-*,
misc-*,
readability-*,
-readability-implicit-bool-conversion,
-readability-magic-numbers,
-readability-identifier-length,
-misc-unused-parameters,
-misc-include-cleaner
-misc-include-cleaner,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling

HeaderFilterRegex: 'src/.*'
HeaderFilterRegex: '^(src|include|cmd)/.*'

CheckOptions:
readability-identifier-naming.VariableCase: lower_case
readability-identifier-naming.ParameterCase: lower_case
readability-identifier-naming.FunctionCase: lower_case
readability-identifier-naming.StructCase: CamelCase
readability-identifier-naming.TypedefCase: CamelCase
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.MacroDefinitionCase: UPPER_CASE
readability-identifier-naming.EnumConstantCase: UPPER_CASE
readability-function-cognitive-complexity.Threshold: 25
readability-function-cognitive-complexity.IgnoreMacros: true
14 changes: 10 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
.DS_Store
*.o
.so
*.dylib
build
*log*.txt

tmp/
build/

# VSCode
.vscode/*
!.vscode/c_cpp_properties.json
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json

# Misc
docs/

7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.DS_Store
*log*.txt

/tmp
/build
tmp/
build/

# VSCode
.vscode/*
Expand All @@ -12,3 +12,6 @@
!.vscode/settings.json
!.vscode/tasks.json

# Misc
docs/

4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug tl_app_test",
"name": "Debug tl_app",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/build/bin/tl_app_test",
"program": "${workspaceFolder}/build/tests/bin/tl_app",
"args": [],
"cwd": "${workspaceFolder}",
"preLaunchTask": "CMake: Build"
Expand Down
23 changes: 23 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"C_Cpp.formatting": "clangFormat",
"C_Cpp.clang_format_style": "file",
"C_Cpp.codeAnalysis.runAutomatically": true,
"C_Cpp.codeAnalysis.clangTidy.enabled": true,
"C_Cpp.files.exclude": {
"**/build": true,
"**/tmp": true
},
"[c]": {
"editor.tabSize": 4,
"editor.insertSpaces": true,
Expand All @@ -9,13 +17,25 @@
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[markdown]": {
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true
},
"files.associations": {
"*.jsonl": "json",
".clang-format": "yaml",
"*.cmake.in": "cmake"
},
"files.watcherExclude": {
"**/build/**": true,
"**/tmp/**": true
},
"search.exclude": {
"**/build": true,
"**/tmp": true
},
"cSpell.words": [
"armv",
Expand All @@ -34,4 +54,7 @@
"trunc",
"xcrun"
],
"chat.tools.terminal.autoApprove": {
"mise": true,
}
}
5 changes: 2 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}/build",
"--config",
"Debug"
"--preset",
"default"
],
"group": {
"kind": "build",
Expand Down
255 changes: 255 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# Project Overview

tinyclib is a tiny C library for building applications across various platforms,
from server-grade hardware to desktops, mobile, and embedded devices.

## Folder Structure

- `build/`: Build directory (not committed to git).
- `cmd/`: Command-line tools.
- `docs/`: Project documentation.
- `examples/`: Example code and usage demonstrations.
- `include/`: Public API headers.
- `scripts/`: Utility scripts which are not part of the main source code.
- `src/`: Source code.
- `tests/`: Test files.
- `third_party/`: Third-party libraries and tools.
- `tmp/`: Temporary files and outputs.

## Project Guidelines

- Pass extensible boundary structs by pointer, not by value. Use `const` pointer
parameters for input structs and mutable pointer parameters for output
structs. Passing a struct by value bakes the full layout into the call ABI and
defeats the future usefulness of the leading `size` field.
- Update all references across the codebase and documentation when you rename
command arguments, config parameters or any other identifiers.
- When defining C structs, order fields strictly to minimize memory padding and
optimize cache alignment, never purely for narrative readability. Sort fields
from largest to smallest alignment requirements (e.g., 8-byte
pointers/`size_t` first, then 4-byte integers, down to 1-byte
booleans/`char`). If specific fields are frequently accessed together in a hot
path, group them sequentially within those size constraints to maximize
cache-line efficiency. Only apply logical grouping (e.g., inputs vs. outputs)
when it does not introduce invisible padding bytes.
- Do not use cross-file global variables. Use file-local static state only when
necessary. Use opaque context types across multi-file module boundaries,
including tests. Use passed-in context structs for per-instance, per-request,
async, or otherwise explicit state.
- Pay attention to forward declarations when you are using C opaque pointers.
- Keep source code, comments, public headers, examples, and non-plan
documentation project-generic. External consumer names and runtime-specific
integration details belong in plan files only.
- Do not use magic numbers. Every numeric literal used as a size, offset, index,
flag mask, or field position must be a named constant or carry an inline
comment explaining its meaning. Trivially obvious values (0, 1, `'\0'`,
null-terminator arithmetic) are exempt.
- Bare zero values are acceptable for explicit "none" or "unused" semantics, but
they must carry a comment. Use an inline comment when the meaning is short,
and a standalone explanatory comment when the field or control flow needs more
context.
- Do not use scope prefixes like g_ in C. Use descriptive snake_case names.
- Always use uppercase letters for numeric literal suffixes (e.g., 123U, 10L,
0Z). Never use lowercase (u, l, uz) to prevent visual ambiguity, especially
the dangerous confusion between the letter l and the number 1. Apply this
uniformly to all macros and literal values.
- When generating C callback functions that require error handling and control
flow, always use an explicitly typed enum (enum : int) as the return type to
guarantee ABI stability without relying on out-parameters. Structure the enum
values strictly into three mathematical ranges: assign negative integers (< 0)
for specific, fatal error states (e.g., -1, -2) so the caller can catch all
failures with a single, fast if (result < 0) branch; assign exactly 0 for the
default success or 'continue' state; and assign positive integers (> 0) for
non-error control flow signals like a graceful halt or skipping an element.
This ensures idiomatic, branch-prediction-friendly error propagation where
negative codes can be immediately returned and bubbled up the call stack.
- Prefer status-returning mutating functions when an operation changes state and
may fail. Return `int` for success or failure, and use pointer parameters for
produced values such as indexes or handles.
- Use C "middle style" naming convention for non-static file-level functions
like `[lib]_[file]_[action]_[property]()`. Use C "prefix style" naming
convention for static functions like `[action]_[property]()`. For
boolean-returning functions, use is_/has_/can_ as the action slot.
- For boolean variables and struct fields, prefer concise state names like done,
ready, enabled, or <noun>_done.
- Comments in test files should explain what the code can't; the source of a
constant, the meaning of magic bytes, why an odd-looking assertion holds. Do
not restate what names and assertions already say. In test files, prefer an
inline comment over extracting a named constant for magic numbers; keep the
literal at the use site so each test stays self-contained. When refactoring
tests, do not delete existing test comments just because a new test name is
more descriptive. Preserve existing comments unless the user explicitly asks
for their removal, or update them in place when the code change makes them
inaccurate.
- When generating or modifying code comments, act as a procedural outlier. Your
goal is to allow a developer to understand the high-level execution flow
simply by reading the comments from top to bottom. Actively document each
logical step and structural block using short, direct statements (e.g.,
"Resolve mount and relative path", "Determine file type"). It is acceptable to
describe the immediate action being taken, provided it acts as a clear section
header that prevents the reader from needing to parse the underlying C syntax.
- When generating or modifying code comments, you must determine capitalization
based on placement and punctuation based on grammatical structure. If a
comment sits on its own line, it must begin with an uppercase letter. If a
comment is placed inline, directly next to code on the same line, it must
begin with a lowercase letter. You must completely omit terminal periods for
all inline comments, short imperative commands, fragments, and noun phrases
(e.g., `// Initialize the database` or
`let x = 0; // initialize the database`). You may only use a terminal period
if a standalone comment is a true, formal full sentence containing both a
distinct subject and a verb.
- Use Doxygen-style documentation comments (`/** ... */`) with tags like @brief,
@param, @return, etc. for all public functions, important internal functions
and macros. Add a new line after each tag group. Use `//` for inline comments.
Do not use comment dividers (`//---...`), or section headers
(`// === ... ===`).
- Start function `@brief` text with a brief third-person singular present-tense
verb, such as "Returns ...", "Handles ...", "Sets ...", "Adds ...` and so on.
- Place documentation comments where the entity is declared, not duplicated
across files. If a function, struct or macro is declared in a header file, its
Doxygen comment lives in the header and the implementation file carries no
repeated block. If the entity has no header declaration, such as a static
function or a file-local struct, its comment lives in the implementation file
where it is defined. The goal is a single source of truth per entity so
comments do not drift apart over time.
- Plan files must begin with `# <title>`, `## Overview`, `## Scope` and
`## Out of Scope` sections, in that order, so a reader can understand what the
plan is about and what it deliberately excludes without reading the whole
document. If the plan has unresolved design questions, follow those three
sections with an `## Open Questions` section. And add a `## Recommendations`
section before any other content
- Do not suppress formatter, linter and checker warnings without asking first.
- Do not remove tests without asking first. Do not cause regressions in existing
tests without asking first.
- When you are creating CMakeLists.txt files for new folders, follow the
existing patterns, structure and naming conventions.
- Do not create common or utility files without asking first.
- Do not add `_internal`, `_private`, `_impl`, or similar suffixes to files,
types, or functions - the directory already carries that signal. Prefer
specific, descriptive filenames (e.g. `client.c`, `request.c`) over generic
ones (`async.c`, `common.c`).
- Stay consistent with existing similar files. Before adding or modifying a
file, look at its siblings and peers to mirror their directory layout, file
organization, section ordering, include/header grouping, naming conventions,
comment style, struct and function shapes, error handling patterns and test
scaffolding. Prefer matching the established pattern over introducing a new
one, even when the new approach seems marginally better. If the existing
pattern is genuinely wrong or no longer fits, ask before diverging so the
inconsistency is intentional and applied across the affected files together.
- Place unit and internal module tests in `tests/unit/`, public API tests in
`tests/api/` and integration tests in `tests/integration/`. Name unit and
internal test files after the module they test using `test_<module>.c`, where
`<module>` matches the source or private header name without the project
prefix. If an internal test covers multiple private modules, use
`test_core_<feature>.c`. For public API tests, use `test_api_<header>.c`,
where `<header>` is the public header name without the project prefix and
without the `.h` suffix. Do not use generic names like `test_utils.c` or
`test_common.c`. Do not mix public API behavior tests and private/internal
module tests in the same file.
- All C tests must use the Unity framework (`unity.h`, `setUp`, `tearDown`,
`UNITY_BEGIN`, `RUN_TEST`, and `UNITY_END`) and be wired through the existing
CMake test target list patterns.
- When writing C test function names, use established pattern structures such as
`test_<function>_<scenario>` for simple utilities,
`test_<function>_<scenario>_<outcome>` for complex algorithms, or
`test_<state>_<action>_<outcome>` for state machines. Within your test file,
always group by target function by clustering all tests for a single function
together to easily spot coverage gaps. Additionally, avoid generic scenario
names like `_normal_case` or `_works` by specifying exactly what makes the
input valid, and remember to test static functions indirectly by evaluating
the public API functions that trigger their logic rather than calling them
directly.
- Known Issues and Bugs: There is a line between "known issues" and "bugs".
Known issues are things that are not ideal but are acceptable for the current
scope and requirements. Bugs are on the other hand unacceptable regardless of
scope. Always add comments and documentation for known issues. Always create a
markdown file in the `docs/bugs` directory for each bug with a description of
the issue. Do not ignore or silently move on from bugs.
- Avoid regressions in existing functionality without asking first.Do not remove
or change a test just to make it pass without asking first.
- **IMPORTANT!** Avoid implicit behavior, "magic" and silently ignoring errors.
Always prefer to report errors, warnings and unsupported use cases.
- **IMPORTANT!** Always ask before making any significant changes related to
performance and public API.
- **IMPORTANT** Do not pollute the project root with temporary files. Use the
project `tmp/` folder for all temporary files and outputs.
- **IMPORTANT!** This is a C project. Do not add any other languages or
dependencies without asking first.
- **IMPORTANT** Do not clean build folder without asking first! Using
`mise exec -- make configure` and `mise exec -- make build` is much faster for
iterative development. Use `mise exec -- make clean` only when you know what
you are doing and need to clean all the generated files.
- Keep tinyclib changes small. Prefer additive public API changes when possible
and call out any breaking public API change before implementing it.

## Quick Reference

- `make help`: Show available make targets
- `make configure`: Configure cmake
- `make build`: Build the project
- `make clean`: Remove temporary directories
- `make test`: Run tests
- `make format`: Check code formatting
- `make lint`: Check code linting
- `make check`: Check types
- `make check-all`: Run all checks
- `make fix`: Fix code formatting and linting issues
- `mise exec -- [COMMAND]`: Run a command with the project environment. This
ensures the correct toolchain (cmake, clang, etc.) is used. For example
`mise exec -- cmake`, `mise exec -- make test`.
- `eval "$(mise activate [SHELL_TYPE])"`: Initializes mise in the current shell
session. For example `eval "$(mise activate bash)"`.

## References

- [Unity](https://github.com/ThrowTheSwitch/Unity.git) See `tmp/unity` folder
for a local copy of the source and documentation.

## Design Notes

### Reviewability Boundaries

For reviewability, keep capability boundaries small, boring and contractual.
Prefer files that own one stable concept over large files that gather unrelated
operations only because they share a handle or backend. A good boundary lets
stable code stay unchanged while related new work is added in nearby focused
files.

- Split public headers and source files by consumer-facing capability.
- Keep high-churn features behind narrow headers, narrow source files and
focused tests so reviewers can understand a change without loading unrelated
behavior into memory.
- Add or maintain compile and behavior tests at each boundary so accidental
coupling, broad includes and behavior regressions are caught quickly.
- Avoid catch-all files and directories. Use specific names that describe the
contract they own, and add new files beside stable contracts when a feature
has a distinct consumer or change pattern.
- Treat mature files as contracts. Once a module has clear invariants and good
coverage, prefer extending through adjacent modules rather than repeatedly
reopening the stable core.

### Abstraction Boundary

The public API (`include/` and `src/`) uses the project's own type and function
names, not the underlying libraries' names.

This boundary applies to the public API surface only. Commands (`cmd/`) and
examples (`examples/`) are free to use third-party libraries directly like a
normal C project.

### Third-Party code patches

When you patch a third-party code, add `.patch` file for each patch and `.md`
file matching the patch name with a content like (similar to "Common Changelog"
categories):

```markdown
# <file_name>.patch

- **Changed** ...
- **Added** ...
- **Removed** ...
- **Fixed** ...
```

Keep it simple, do not add unnecessary details.
Loading