STC (Smart Template Containers)

repository·main·Indexed 24 days ago

https://github.com/stclib/stc

A high-performance, type-safe container and algorithm library for C99/C11. STC provides modern abstractions including smart pointers (arc, box), sum types, and coroutines. It features generic templated data structures and algorithms with a focus on memory management, minimal boilerplate, and signed arithmetic to prevent common bugs. The library is a hybrid of header-only templated containers and a shared library (libstc) for specific types like cstr, csview, cregex, cspan, and random.

Tokens
53.6K
Snippets
86
Records
188
Agent score
84%

What's inside STC

  1. Overview of STC - Smart Template Containers

    main

    STC is a comprehensive, general-purpose container and algorithm library for C99/C11. It provides high-performance, type-safe, and generic (templated) data structures and algorithms similar to those found in modern languages like Rust, Zig, and C++.

    Key features include:

    • Type Safety: Uses templating to avoid error-prone casting and opaque pointers.
    • Memory Management: Fully managed containers that use user-supplied or default drop functions for destruction. Supports smart pointers like arc (shared) and box (unique).
    • Performance: Optimized implementations, including Robin Hood hashing for hmap.
    • Ergonomics: Minimal boilerplate, uniform APIs across containers, and support for ranged for-loops.
    • Signed Arithmetic: Uses signed integers for indices and sizes to prevent common unsigned/signed mixing bugs.
  2. Overview of STC Coroutines

    main

    STC provides a small, portable, and ergonomic implementation of stackless, fully typesafe coroutines in C99. It is designed for lightweight concurrency, making it suitable for memory-constrained or embedded systems.

    Key Features:

    • Concurrency Model: Supports spawning concurrent tasks using fibers (green threads) managed by an internal scheduler. Tasks can be organized into task-groups that must be awaited to ensure completion.
    • Control Flow: Supports both asymmetric/structured concurrency and symmetric transfer of control.
    • Error Handling: Provides strong error handling where users can throw errors and recover from them at the original suspension point. Unhandled errors will cause the program to exit with a message including the line number of the throw.
    • Memory Management:
      • Fibers: Allocated on the heap and automatically freed by the scheduler.
      • Coroutine Frames: Provided by the user. They can be stack-allocated (common for lightweight usage) or heap-allocated.
      • Cleanup: Coroutine frames/objects are cleaned up at the cco_finalize: label inside the cco_async scope. This occurs during normal completion, errors, or cancellation.
    • Efficiency: Very low overhead (~100 bytes per fiber, ~50 bytes per coroutine object/task).
  3. What is STC arc and how does it work?

    main

    arc is an atomic reference-counted smart pointer that enables shared ownership of an object. Multiple arc objects can own the same object; the object is only destroyed and its memory deallocated when the last remaining arc owning it is destroyed via arc_X_drop().

    Key characteristics:

    • Thread-safety: All arc functions can be called by multiple threads on different instances of arc without additional synchronization, even if they share ownership of the same object, thanks to atomic reference counting.
    • Empty state: An arc can own no objects (it is
    Arc a1 = Arc_make(value);
    Value* vp = Arc_move(&a1).get; // a1 will now be NULL
    Arc a2 = Arc_toarc(vp);
    c_drop(Arc, &a1, &a2);
  4. What is STC smap (Sorted Map)?

    main

    A smap is a sorted associative container that stores unique key-value pairs. Keys are kept in sorted order using a comparison function (keyCompare).

    Key Characteristics:

    • Complexity: Search, removal, and insertion operations have logarithmic complexity $O(\log n)$.
    • Implementation: Uses an AA-tree, which provides a flatter, more balanced structure than traditional red-black trees.
    • Iterator Invalidation:
      • Iterators are invalidated after insert or erase operations.
      • References to elements are only invalidated after an erase operation.
      • To safely erase elements while iterating, use the iterator returned by erase_at(), which points to the next element, or use erase_range().
  5. What is csview and how to use it

    main

    A csview is a non-zero terminated and utf8-iterable string view. It represents a constant contiguous sequence of characters and is implemented as a pointer to a constant char and a size.

    Key characteristics:

    • No memory allocation: It does not allocate memory and does not need to be destructed.
    • Lifetime: Its lifetime is limited by the source string storage.
    • Performance: It stores the length, avoiding strlen() calls.
    • Printing: Because it is not zero-terminated, you must use the c_svarg macro with the c_svfmt format specifier when using printf or similar functions.

    Use csview when you need a lightweight, read-only view of a string (like a substring or a slice) without the overhead of copying or allocating new memory.

    csview sv = c_sv("Hello world");
    sv = csview_subview(sv, 0, 5);
    printf(c_svfmt "\n", c_svarg(sv)); // "Hello"
  6. What is an STC hmap and how does it work?

    main

    An hmap is an associative container that stores unique key-value pairs. It provides average constant-time complexity for search, insertion, and removal operations.

    Key Characteristics:

    • Internal Organization: Elements are organized into buckets based on the hash of their key. They are not stored in any particular order.
    • Implementation: It uses closed hashing (open addressing) with linear probing and does not leave tombstones on erase.
    • Iterator Invalidation:
      • Erase: References and iterators are invalidated after an erase operation.
      • Insert: No iterators are invalidated unless the hash table needs to be extended (resized).
    • Performance Tip: You can call hmap_X_reserve() prior to insertions if the maximum size is known to prevent reallocations.
    • Safe Erase during Iteration: To erase elements while iterating, use hmap_X_erase_at(). It returns the iterator to the next element. Note that a small number of elements may be visited twice, but all will be visited.
  7. Define and Manage Tasks and Fibers

    main

    Tasks are coroutine function-objects, and Fibers are green-thread-like entities within a system thread.

    Defining Custom Tasks

    Use cco_task_struct to extend the base cco_task structure:

    • cco_task_struct(name): Defines a custom task struct.
    • cco_task_struct(name, <Data>*=void*, MAX_GROUPS=1): Allows specifying a pointer type returned by cco_data() and the maximum number of waitgroups for cco_group(index). Default MAX_GROUPS is 1.

    Task Control

    • cco_yield_to(cco_task* task): Performs a symmetric transfer of control to another task.
    • int cco_resume(cco_task* task): Resumes a task until it suspends (blocking). Returns the status.

    Task Data Accessors

    • Data* cco_data(cco_task* task): Retrieves the auxiliary data pointer stored in the task's associated fiber.
    • Data* cco_set_data_ptr(cco_task* task, Data* dt): Sets the auxiliary data pointer for a task.
    • cco_task_fiber(cco_task* task): Retrieves the fiber associated with a specific task.
                    cco_task_struct(name) {<name>_base base; ..};
                    cco_task_struct(name, <Data>*=void*, MAX_GROUPS=1)  
    
                    cco_yield_to(cco_task* task);                       
    int             cco_resume(cco_task* task);                         
    
                    int             cco_status();                                       
    cco_fiber*      cco_task_fiber(cco_task* task);                     
    
    Data*           cco_data(cco_task* task);                           
    Data*           cco_set_data_ptr(cco_task* task, Data* dt);         
  8. Manage Coroutine Frame Allocation

    main

    Since STC coroutines are stackless, you must provide the coroutine frame which holds local variables and I/O. You have two primary strategies for allocation:

    1. Stack Allocation (Recommended for lightweight/embedded): Let each coroutine frame store the frames of coroutines it calls or awaits, typically on the stack. This keeps coroutines extremely lightweight.
    2. Heap Allocation: Allocate frames individually on the heap just before they are called or awaited. Important: If you use heap allocation, each coroutine must manually free itself between the end of the cco_async scope and the return 0; statement.
  9. Understand the zsview concept

    main

    zsview is a zero-terminated and utf8-iterable string view. It represents a constant contiguous sequence of characters starting at position zero.

    Key Characteristics

    • Zero-terminated: Unlike csview (which is a non-zero-terminated span), zsview is guaranteed to be null-terminated. This makes it an efficient replacement for const char*, as you can safely pass zv.str to C APIs expecting standard C-strings.
    • No Allocation: It never allocates memory and does not require destruction. Its lifetime is tied to the underlying source string storage.
    • Efficient: It stores the string length, eliminating the need for repeated strlen() calls.

    Underlying Structure

    • zsview: struct { const char *str; isize_t size; }
    • zsview_value: const char (the element type)
    • zsview_iter: union { zsview_value *ref; csview chr; } (the UTF8 iterator type)
  10. Handle errors in Tasks using cco_throw and cco_recover

    main

    Tasks support a structured error handling mechanism similar to exceptions. An error can be thrown within a coroutine using cco_throw(value), which unwinds the call stack.

    To handle an error, check cco_error() within a cco_async scope. If an error is detected, you can use the cco_recover statement to resume control at the original suspension point in the current task. This allows higher-level tasks to catch and recover from failures in nested subtasks.

    int TaskA(struct TaskA* o) {
        cco_async (o) {
            printf("TaskA start: %d\n", o->a);
    
            cco_await_task(&cco_data(o)->task_b);
    
            cco_finalize:
            if (cco_error() == 99) {
                printf("TaskA recovered error '99' thrown on line %d\n", cco_err().line);
                cco_recover;
            }
            puts("TaskA done");
        }
        return 0;
    }
  11. Use i_class_key for complex types in containers

    main

    When a container holds a type that has its own _clone() and _drop() members (like another STC container), use the i_class_key template parameter to tell STC to use those members for deep copying and destruction.

    #define T Vec2D
    #define i_class_key Vec
    #include <stc/vec.h>
  12. How alternative 'raw' types work for efficiency

    main

    STC allows you to use a 'raw' type for lookups and emplace operations to avoid unnecessary allocations or conversions. For example, a cstr (string) container might use a const char* as the RawKeyType for lookups, while the actual stored KeyType is a managed string object.

    To implement this, you can use the i_comp_key parameter or the c_comp_key trait. This binds several conversion and comparison functions:

    1. i_keyraw: Defines the RawKeyType.
    2. i_keyfrom: A function to create a KeyType from a RawKeyType.
    3. i_keytoraw: A function to convert a KeyType* to a RawKeyType. This is required if i_keyraw is defined.
    4. Comparison functions (i_cmp, i_eq, i_hash) are then bound to the RawType versions (e.g., RawType_cmp()).

    This mechanism is used by library types like cstr, box, and arc to enhance ergonomics and performance.