Tutorial Quick Links šŗļø
This code is a part of the Daedalus library.
The content of this tutorial is designed to help you understand how to create an efficient string builder for your own projects.
Part 1: Beyond the Basics - Why We Need More
Part 2: Crafting New Capabilities - The Code Deep Dive
Part 1: Beyond the Basics - Why We Need More
Hey everyone! If you've been following along, you know we've already built a pretty solid foundation for handling strings in C with our dString_t
builder. We tackled the frustrating parts of manual memory management and made basic string appending safe and efficient. It was all about creating a reliable core.
But here's the thing about building tools for real-world projects, especially something as dynamic as a game engine: the basics are just the beginning. While d_AppendToString
is fantastic for simple concatenation, what happens when you need to format complex messages with multiple variables? Or build intricate text-based user interfaces? Or perform advanced manipulations like joining lists of words or extracting specific parts of a string with flexible indexing?
That's where we're headed today! This post is all about expanding the dString_t
library with a new set of powerful functions. We'll explore how these additions make our string builder even more versatile, allowing us to write cleaner, more expressive code for tasks that often become cumbersome in C. It's about pushing the boundaries of what's comfortable and discovering new ways to make C development more enjoyable and productive. Let's dive in and see what new capabilities we can unlock!
Setting Up Your Workspace: The Daedalus.h Header
To ensure everything compiles smoothly and to provide a clear interface for our library, all dString_t
functions, including the structure itself and its associated constants, are declared in a central header file, typically Daedalus.h
. When you include this header in your C source files, your compiler gains full knowledge of these tools, allowing for proper type checking and function linking. Think of Daedalus.h
as the blueprint that tells your code how to interact with the string builder.
// --- Daedalus.h (Content for your primary Daedalus header file) ---
#ifndef DAEDALUS_H
#define DAEDALUS_H
// Standard C library includes that Daedalus string functions rely on
#include <stdio.h> // For snprintf, vsnprintf, fprintf
#include <stdlib.h> // For malloc, calloc, realloc, free
#include <string.h> // For strlen, memmove, memcpy, strcmp, strchr, strncpy
#include <stdbool.h> // For bool type
#include <stdarg.h> // For va_list (essential for variadic functions like d_FormatString)
#include <stddef.h> // For ptrdiff_t (used in d_AppendToString for self-append safety)
// The dString_t structure definition - the core data structure for our string builder
typedef struct {
char* str; /**< Pointer to the dynamically allocated character buffer. */
size_t alloced; /**< Total allocated memory in bytes for the 'str' buffer. This is its current capacity. */
size_t len; /**< Current length of the string stored in 'str', *excluding* the null terminator. */
} dString_t;
// Minimum initial size for string builder allocation. A small, reasonable default.
static const size_t d_string_builder_min_size = 32;
// --- Core Lifecycle Functions (from previous blog posts) ---
// These functions manage the creation and destruction of dString_t instances.
dString_t* d_InitString(void);
void d_DestroyString(dString_t* sb);
// --- Core Append Functions (from previous blog posts) ---
// The fundamental methods for adding various data types to the string builder.
void d_AppendToString(dString_t* sb, const char* str, size_t len);
void d_AppendToStringN(dString_t* sb, const char* str, size_t max_len);
void d_AppendCharToString(dString_t* sb, char c);
void d_AppendIntToString(dString_t* sb, int val);
void d_AppendFloatToString(dString_t* sb, float val, int decimals);
// --- String Manipulation Functions (from previous blog posts) ---
// Tools for modifying the existing content of a dString_t.
int d_SetString(dString_t* string, const char* content, int flags);
void d_ClearString(dString_t* sb);
void d_TruncateString(dString_t* sb, size_t len);
void d_DropString(dString_t* sb, size_t len);
dString_t* d_CloneString(const dString_t* source);
// --- Accessor Functions (from previous blog posts) ---
// Methods to safely inspect the dString_t's content and properties.
size_t d_GetLengthOfString(const dString_t* sb);
const char* d_PeekString(const dString_t* sb);
char* d_DumpString(const dString_t* sb, size_t* len);
// --- Internal Helper Function (declared here, defined in the .c file) ---
// This static function handles the automatic memory growth for the string builder.
static void d_StringBuilderEnsureSpace(dString_t* sb, size_t add_len);
// --- New Advanced Functions (declared here, implemented in your .c file) ---
// These are the exciting new capabilities we'll explore in this blog post!
// Formatted String Generation (printf-style appending)
void d_FormatString(dString_t* sb, const char* format, ...);
// Rich Text UI Elements (for building dynamic console interfaces)
void d_RepeatString(dString_t* sb, char character, int count);
void d_AppendProgressBarToString(dString_t* sb, int current, int max, int width, char fill_char, char empty_char);
void d_PadLeftString(dString_t* sb, const char* text, int width, char pad_char);
void d_PadRightString(dString_t* sb, const char* text, int width, char pad_char);
void d_PadCenterString(dString_t* sb, const char* text, int width, char pad_char);
// Intuitive String Manipulation (Pythonic utilities for common tasks)
void d_ApplyTemplateToString(dString_t* sb, const char* tmplt, const char** keys, const char** values, int count);
void d_JoinStrings(dString_t* sb, const char** strings, int count, const char* separator);
void d_SliceString(dString_t* sb, const char* text, int start, int end);
int d_CompareStrings(const dString_t* str1, const dString_t* str2);
#endif // DAEDALUS_H
// --- End of Daedalus.h example content ---
Core dString_t
Function Implementations (for context)
For completeness and to ensure the examples in this post are runnable, here are the core implementations of d_StringBuilderEnsureSpace
and a few essential dString_t
functions. These were covered in detail in previous parts of this series, so we won't re-explain them here. They are included to provide the necessary context for the new functions we're about to explore. You would place these in your Daedalus.c
file (or similar implementation file).
// --- Content for your Daedalus.c (or similar) implementation file ---
// Include the main Daedalus header
#include "Daedalus.h"
// --- Core d_StringBuilderEnsureSpace implementation (from previous parts) ---
// This function is static, meaning it's only visible within this .c file.
// It handles the automatic memory reallocation for the string builder.
static void d_StringBuilderEnsureSpace(dString_t* sb, size_t add_len)
{
if (sb == NULL || add_len == 0) return;
// Check if current allocated space is sufficient for current length + new data + null terminator
if (sb->alloced >= sb->len + add_len + 1) return;
// If not enough space, grow the buffer using a doubling strategy
while (sb->alloced < sb->len + add_len + 1) {
sb->alloced <<= 1; // Double the allocated size
if (sb->alloced == 0) {
// Handle potential overflow if alloced was already very large, setting it to max size_t.
sb->alloced = (size_t)-1;
break;
}
}
char* new_str = realloc(sb->str, sb->alloced);
if (new_str == NULL) {
// Handle reallocation failure: in a robust system, this might
// trigger a global error state or propagate an error code.
// For this tutorial, we simply return, indicating a failure to grow.
// The sb->str will remain its old value, potentially leading to truncation
// if subsequent appends are attempted without enough space.
return;
}
sb->str = new_str; // Update the string pointer to the potentially new memory location
}
// --- Example core dString_t function implementations (from previous parts) ---
// These are included for context and to make the new examples runnable.
/**
* @brief Create a new string builder.
* @return dString_t* A new string builder, or NULL on allocation failure.
*/
dString_t* d_InitString(void) {
dString_t* sb = calloc(1, sizeof(*sb)); // Allocate and zero-initialize the struct
if (sb == NULL) return NULL;
sb->str = malloc(d_string_builder_min_size); // Allocate initial buffer
if (sb->str == NULL) {
free(sb); // Clean up struct if buffer allocation fails
return NULL;
}
*sb->str = '\0'; // Initialize as an empty C string
sb->alloced = d_string_builder_min_size;
sb->len = 0;
return sb;
}
/**
* @brief Destroy a string builder and free its memory.
* @param sb Pointer to string builder to destroy.
*/
void d_DestroyString(dString_t* sb) {
if (sb == NULL) return;
free(sb->str); // Free the internal string buffer
free(sb); // Free the dString_t struct itself
}
/**
* @brief Add a C string to the string builder.
* @param sb The string builder to append to.
* @param str The string to append (must be null-terminated if len is 0).
* @param len The length of string to append, or 0 to use strlen().
*/
void d_AppendToString(dString_t* sb, const char* str, size_t len) {
if (sb == NULL || str == NULL) return;
if (len == 0) { if (*str == '\0') return; len = strlen(str); }
// Handle self-append scenario where source string is part of the builder's own buffer.
// Save the offset, as the base pointer (sb->str) might change after realloc.
ptrdiff_t offset = -1;
if (str >= sb->str && str < sb->str + sb->alloced) {
offset = str - sb->str;
}
d_StringBuilderEnsureSpace(sb, len); // Ensure enough space, potentially reallocating
if (sb->str == NULL) return; // Check for realloc failure
const char* source_ptr = str;
if (offset != -1) {
// If it was a self-append, recalculate the source pointer based on the new sb->str.
source_ptr = sb->str + offset;
}
memmove(sb->str + sb->len, source_ptr, len); // Use memmove for safe overlapping copies
sb->len += len; // Update length
sb->str[sb->len] = '\0'; // Ensure null-termination
}
/**
* @brief Add a single character to the string builder.
* @param sb The string builder to append to.
* @param c The character to append.
*/
void d_AppendCharToString(dString_t* sb, char c) {
if (sb == NULL) return;
d_StringBuilderEnsureSpace(sb, 1); // Ensure space for 1 char + null terminator
if (sb->str == NULL) return;
sb->str[sb->len] = c;
sb->len++;
sb->str[sb->len] = '\0';
}
/**
* @brief Add an integer to the string builder as a decimal string.
* @param sb The string builder to append to.
* @param val The integer value to append.
*/
void d_AppendIntToString(dString_t* sb, int val) {
char temp_str[12]; // Sufficient for 32-bit int + sign + null
if (sb == NULL) return;
snprintf(temp_str, sizeof(temp_str), "%d", val);
d_AppendToString(sb, temp_str, 0);
}
/**
* @brief Get a read-only pointer to the string builder's content.
* @param sb Pointer to string builder.
* @return const char* Pointer to internal string, or NULL if sb is NULL.
*/
const char* d_PeekString(const dString_t* sb) {
if (sb == NULL) return NULL;
return sb->str;
}
/**
* @brief Clear the string builder content.
* @param sb The string builder to clear.
*/
void d_ClearString(dString_t* sb) {
if (sb == NULL) return;
sb->len = 0;
if (sb->str != NULL) { // Ensure str is not NULL before dereferencing
sb->str[0] = '\0'; // Set first char to null terminator, effectively clearing
}
}
/**
* @brief Get the current length of the string builder content.
* @param sb Pointer to string builder.
* @return size_t Current length in characters, or 0 if sb is NULL.
*/
size_t d_GetLengthOfString(const dString_t* sb) {
if (sb == NULL) return 0;
return sb->len;
}
/**
* @brief Helper to check if a dString_t object is invalid.
* @param sb Pointer to the dString_t object.
* @return True if the dString_t is NULL or its internal string pointer is NULL, false otherwise.
*/
static bool d_IsStringInvalid(const dString_t* sb) {
return (sb == NULL || sb->str == NULL);
}
// --- End of core function implementations for context ---
Part 2: Crafting New Capabilities - The Code Deep Dive
1. Printf-Style Formatting
The need to combine static text with dynamic data is omnipresent in game development, from intricate status readouts to verbose debug logs. While basic d_AppendToString
handles simple concatenation, a more powerful, printf
-like capability is often desired. d_FormatString
provides this functionality to the dString_t
, allowing for precise formatting of complex messages with automatic memory management. This function leverages the vsnprintf
standard library function, ensuring type-safe formatting and preventing buffer overflows by dynamically resizing the dString_t
's internal buffer to accommodate the generated text. It's a powerful way to build dynamic strings with familiar formatting specifiers.
/**
* @brief Append formatted text to the string builder using printf-style formatting.
*
* @param sb Pointer to string builder.
* @param format Printf-style format string.
* @param ... Variable arguments corresponding to format specifiers.
*
* @note Uses vsnprintf internally for safe formatting.
* @note Supports all standard printf format specifiers (%d, %s, %f, etc.).
* @note Automatically calculates required space and grows buffer as needed.
* @note Appends formatted text to existing content (does not replace).
*/
void d_FormatString(dString_t* sb, const char* format, ...) {
if (sb == NULL || format == NULL) return;
va_list args;
va_start(args, format);
// First call to vsnprintf with NULL buffer and 0 size to determine required length.
// This is a standard C technique for safely determining the buffer size needed
// for formatted output without risking overflows.
va_list args_copy;
va_copy(args_copy, args); // Create a copy of va_list as it's consumed by vsnprintf
int needed = vsnprintf(NULL, 0, format, args_copy);
va_end(args_copy); // Clean up the copy
if (needed < 0) { // vsnprintf returns negative on encoding error or if buffer is too small (shouldn't happen with NULL buffer)
va_end(args);
return;
}
// Ensure enough space in the dString_t's buffer for the new formatted text
// plus the null terminator. This might trigger a realloc.
d_StringBuilderEnsureSpace(sb, needed);
if (sb->str == NULL) { // Check for reallocation failure, which means out of memory
va_end(args);
return;
}
// Second call to vsnprintf to actually write the formatted string into the buffer.
// We write starting from the current end of the string (sb->str + sb->len).
// The 'needed + 1' ensures space for the null terminator.
vsnprintf(sb->str + sb->len, needed + 1, format, args);
sb->len += needed; // Update the actual length of the string builder
sb->str[sb->len] = '\0'; // Explicitly ensure null-termination at the new end
va_end(args); // Clean up the original va_list
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header to access dString_t and its functions
int main() {
dString_t* game_event = d_InitString();
if (game_event == NULL) {
fprintf(stderr, "Failed to initialize string builder! Out of memory, perhaps?\n");
return 1;
}
const char* hero_name = "Ariadne";
int monster_count = 3;
float gold_found = 12.75f;
// Craft a verbose event message with dynamic data, all safely managed.
d_FormatString(game_event, "The brave %s defeated %d goblins and found %.2f gold!",
hero_name, monster_count, gold_found);
printf("Game Log: %s\n", d_PeekString(game_event));
// Expected Output: Game Log: The brave Ariadne defeated 3 goblins and found 12.75 gold!
// Append another formatted message
d_FormatString(game_event, " A new quest awaits!");
printf("Updated Log: %s\n", d_PeekString(game_event));
// Expected Output: Updated Log: The brave Ariadne defeated 3 goblins and found 12.75 gold! A new quest awaits!
d_DestroyString(game_event); // Always clean up your resources!
return 0;
}
*/
2. Building Rich Text UI Elements
In ASCII games, the user interface is meticulously crafted from characters, forming the very visual language of the game world. Precision in layout and dynamic visual feedback are paramount for an immersive experience. Daedalus provides specialized functions to construct common UI elements like progress bars and precisely padded text, allowing for dynamic, visually appealing menus and status displays with inherent robustness. This is where the art of code meets the canvas of the console, giving detailed life to your digital creations.
d_RepeatString
(An Internal Mechanism for Repetition)
This function, though primarily an internal helper, is a fundamental mechanism that efficiently appends repeated characters. It forms the core building block for constructing visual patterns, such as the filled and empty segments of progress bars or the padding in aligned text. It's a simple, yet powerful, utility that underpins several higher-level UI functions.
/**
* @brief Add repeated characters to the string builder.
*
* @param sb Pointer to string builder.
* @param character Character to repeat.
* @param count Number of times to repeat the character.
*
* @note Does nothing if sb is NULL or count <= 0.
* @note Efficiently adds multiple copies of the same character by direct buffer manipulation.
* @note Used internally by progress bar and padding functions.
*/
void d_RepeatString(dString_t* sb, char character, int count) {
if (sb == NULL || count <= 0) return;
// Ensure sufficient space for 'count' characters plus the null terminator.
// This call handles potential reallocations automatically.
d_StringBuilderEnsureSpace(sb, count);
if (sb->str == NULL) return; // Check for realloc failure
// Directly fill the buffer with the repeated character, starting from the current end.
for (int i = 0; i < count; i++) {
sb->str[sb->len + i] = character;
}
sb->len += count; // Update the length of the string builder
sb->str[sb->len] = '\0'; // Ensure null-termination at the new end
}
d_AppendProgressBarToString
(Visualizing Progress)
A dynamic progress bar is essential for displaying critical game states like health, mana, or loading status. This function constructs a visually intuitive ASCII bar within the builder, dynamically adjusting its filled and empty segments based on the provided values. It's a small detail that adds significant polish and clarity to a text-based interface.
/**
* @brief Add an ASCII progress bar to the string builder.
*
* @param sb Pointer to string builder.
* @param current Current progress value.
* @param max Maximum progress value.
* @param width Width of the progress bar (excluding brackets).
* @param fill_char Character to use for filled portions.
* @param empty_char Character to use for empty portions.
*
* @note Does nothing if sb is NULL, width <= 0, or max <= 0.
* @note Progress bar format: `[āāāāāāāā----]` where `ā` is `fill_char` and `-` is `empty_char`.
* @note If `current > max`, the bar is filled completely.
* @note If `current < 0`, the bar is empty.
* @note Total visual width is `width + 2` (for brackets).
*/
void d_AppendProgressBarToString(dString_t* sb, int current, int max, int width, char fill_char, char empty_char) {
if (sb == NULL || width <= 0 || max <= 0) return;
int filled = 0;
// Calculate filled portion, guarding against division by zero and negative values.
if (current > 0 && max > 0) {
filled = (current * width) / max;
}
if (filled > width) filled = width; // Clamp filled portion to max width
if (filled < 0) filled = 0; // Ensure filled is not negative
d_AppendCharToString(sb, '['); // Append the opening bracket
d_RepeatString(sb, fill_char, filled); // Append the filled portion
d_RepeatString(sb, empty_char, width - filled); // Append the empty portion
d_AppendCharToString(sb, ']'); // Append the closing bracket
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header
int main() {
dString_t* player_status = d_InitString();
if (player_status == NULL) {
fprintf(stderr, "Failed to initialize string builder!\n");
return 1;
}
d_AppendToString(player_status, "Health: ", 0);
d_AppendProgressBarToString(player_status, 75, 100, 20, '#', '-'); // 75% full, 20 chars wide
d_AppendToString(player_status, " Mana: ", 0);
d_AppendProgressBarToString(player_status, 20, 50, 10, '*', '.'); // 40% full, 10 chars wide
printf("%s\n", d_PeekString(player_status));
// Expected Output: Health: [###############-----] Mana: [****......]
d_DestroyString(player_status);
return 0;
}
*/
Padding Functions (d_PadLeftString
, d_PadRightString
, d_PadCenterString
)
For precise textual alignment within ASCII menus and tables, padding functions are indispensable. These functions ensure characters are positioned accurately, creating structured and readable interfaces that guide the user's eye through the presented information.
/**
* @brief Add text padded to the left with specified character to reach target width.
*
* @param sb Pointer to string builder.
* @param text Text to pad (must be null-terminated).
* @param width Target total width including padding.
* @param pad_char Character to use for padding.
*
* @note Does nothing if sb is NULL or text is NULL.
* @note If text length >= width, adds text without padding.
* @note Padding is added to the left side of the text.
* @note Example: `d_PadLeftString(sb, "Hi", 5, '.')` produces `"...Hi"`.
* @note Commonly used for right-aligned text in tables and menus.
*/
void d_PadLeftString(dString_t* sb, const char* text, int width, char pad_char) {
if (sb == NULL || text == NULL || width <= 0) return;
int text_len = strlen(text);
int pad_needed = width - text_len;
if (pad_needed > 0) {
d_RepeatString(sb, pad_char, pad_needed);
}
d_AppendToString(sb, text, 0);
}
/**
* @brief Add text padded to the right with specified character to reach target width.
*
* @param sb Pointer to string builder.
* @param text Text to pad (must be null-terminated).
* @param width Target total width including padding.
* @param pad_char Character to use for padding.
*
* @note Does nothing if sb or text is NULL.
* @note If text length >= width, adds text without padding.
* @note Padding is added to the right side of the text.
* @note Example: `d_PadRightString(sb, "Hi", 5, '.')` produces `"Hi..."`.
* @note Commonly used for left-aligned text in tables and menus.
*/
void d_PadRightString(dString_t* sb, const char* text, int width, char pad_char) {
if (sb == NULL || text == NULL || width <= 0) return;
int text_len = strlen(text);
int pad_needed = width - text_len;
d_AppendToString(sb, text, 0);
if (pad_needed > 0) {
d_RepeatString(sb, pad_char, pad_needed);
}
}
/**
* @brief Add text centered with specified padding character to reach target width.
*
* @param sb Pointer to string builder.
* @param text Text to center (must be null-terminated).
* @param width Target total width including padding.
* @param pad_char Character to use for padding.
*
* @note Does nothing if sb or text is NULL.
* @note If text length >= width, adds text without padding.
* @note Text is centered with padding distributed evenly on both sides.
* @note If padding cannot be evenly distributed, left side gets one less character.
* @note Example: `d_PadCenterString(sb, "Hi", 6, '.')` produces `"..Hi.."`.
* @note Example: `d_PadCenterString(sb, "Hi", 7, '.')` produces `"..Hi..."`.
* @note Commonly used for centered headers and titles in ASCII interfaces.
*/
void d_PadCenterString(dString_t* sb, const char* text, int width, char pad_char) {
if (sb == NULL || text == NULL || width <= 0) return;
int text_len = strlen(text);
int pad_needed = width - text_len;
if (pad_needed <= 0) { // No padding needed or text is too long
d_AppendToString(sb, text, 0);
return;
}
int left_pad = pad_needed / 2;
int right_pad = pad_needed - left_pad; // Ensures total padding is correct even if odd
d_RepeatString(sb, pad_char, left_pad);
d_AppendToString(sb, text, 0);
d_RepeatString(sb, pad_char, right_pad);
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header
int main() {
dString_t* game_menu = d_InitString();
if (game_menu == NULL) {
fprintf(stderr, "Failed to initialize string builder!\n");
return 1;
}
d_PadCenterString(game_menu, "DAEDALUS ADVENTURE", 40, '=');
d_AppendCharToString(game_menu, '\n');
d_PadLeftString(game_menu, "Start New Journey", 40, ' ');
d_AppendCharToString(game_menu, '\n');
d_PadRightString(game_menu, "Continue Saga", 40, ' ');
d_AppendCharToString(game_menu, '\n');
d_PadCenterString(game_menu, "Exit Labyrinth", 40, '-');
printf("%s\n", d_PeekString(game_menu));
// Expected Output:
// =========DAEDALUS ADVENTURE=========
// Start New Journey
// Continue Saga
// -------------Exit Labyrinth-------------
d_DestroyString(game_menu);
return 0;
}
*/
3. Intuitive String Manipulation (Pythonic Utilities)
Daedalus strives to infuse the raw power of C with the intuitive usability found in higher-level languages. Functions that mimic Python's elegant string manipulations, such as joining arrays of strings or slicing substrings with flexible indices, bring unparalleled convenience and readability to C text processing. This is where the spirit of open-source collaboration truly shines, as we adapt proven patterns to empower C developers with modern capabilities, making complex tasks more approachable.
d_ApplyTemplateToString
(Crafting Dynamic Narratives)
For complex, dynamic text generation, such as dialogue or quest descriptions, d_ApplyTemplateToString
allows you to define templates with placeholders that are substituted at runtime. This function is a key component for building flexible and maintainable text systems, enabling game designers to create rich, parameterized text without complex procedural logic.
/**
* @brief Add text with template substitution to the string builder.
*
* @param sb Pointer to string builder.
* @param tmplt Template string with placeholders in {key} format.
* @param keys Array of key strings to match against placeholders.
* @param values Array of replacement values corresponding to keys.
* @param count Number of key-value pairs.
*
* @note Does nothing if sb or tmplt is NULL.
* @note Placeholders must be in format `{keyname}` with no spaces.
* @note Keys are matched exactly (case-sensitive).
* @note If a placeholder has no matching key, it is left unchanged.
* @note Keys longer than 255 characters are treated as literal text to prevent buffer overflows in internal key buffer.
* @note Supports nested braces by treating unmatched `'{'` as literal characters.
*/
void d_ApplyTemplateToString(dString_t* sb, const char* tmplt, const char** keys, const char** values, int count) {
if (sb == NULL || tmplt == NULL) return;
const char* pos = tmplt;
while (*pos) {
if (*pos == '{') {
const char* end = strchr(pos + 1, '}'); // Find the closing brace
if (end) {
// Extract the key name from within the braces
int key_len = end - pos - 1;
char key[256]; // Temporary buffer for the extracted key name
// Ensure the extracted key fits in our temporary buffer and is valid (not empty)
if (key_len < 256 && key_len >= 0) {
strncpy(key, pos + 1, key_len);
key[key_len] = '\0'; // Null-terminate the extracted key string
// Attempt to find a matching value for the extracted key
int found = 0;
if (keys != NULL && values != NULL) { // Ensure key/value arrays are provided
for (int i = 0; i < count; i++) {
if (keys[i] != NULL && strcmp(keys[i], key) == 0) {
if (values[i] != NULL) {
d_AppendToString(sb, values[i], 0); // Append the replacement value
}
found = 1;
break; // Key found, exit loop
}
}
}
if (!found) {
// If no match was found (or keys/values were NULL), append the original placeholder text
d_AppendToString(sb, pos, end - pos + 1);
}
pos = end + 1; // Move past the '}' to continue template parsing
} else {
// Key too long or invalid (e.g., "{}" or "{long_key_...}"), treat '{' as literal
d_AppendCharToString(sb, *pos++);
}
} else {
// Unmatched '{' (no closing '}'), treat it as a literal character
d_AppendCharToString(sb, *pos++);
}
} else {
// Regular character, just append it to the string builder
d_AppendCharToString(sb, *pos++);
}
}
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header
int main() {
dString_t* dialogue_line = d_InitString();
if (dialogue_line == NULL) {
fprintf(stderr, "Failed to initialize string builder!\n");
return 1;
}
const char* template_str = "Greetings, {player_name}. The {faction} thanks you for completing the {quest_name}!";
const char* keys[] = {"player_name", "faction", "quest_name"};
const char* values[] = {"Sir Theseus", "Athenian Guard", "Minotaur's Labyrinth"};
int num_pairs = 3;
d_ApplyTemplateToString(dialogue_line, template_str, keys, values, num_pairs);
printf("Dialogue: %s\n", d_PeekString(dialogue_line));
// Expected Output: Dialogue: Greetings, Sir Theseus. The Athenian Guard thanks you for completing the Minotaur's Labyrinth!
// Example with a missing key (placeholder will remain in the output)
d_ClearString(dialogue_line);
const char* template_str_missing = "Hello {user}, your level is {level}.";
const char* keys_missing[] = {"user"};
const char* values_missing[] = {"Alice"};
d_ApplyTemplateToString(dialogue_line, template_str_missing, keys_missing, values_missing, 1);
printf("Dialogue (missing key): %s\n", d_PeekString(dialogue_line));
// Expected Output: Dialogue (missing key): Hello Alice, your level is {level}.
d_DestroyString(dialogue_line);
return 0;
}
*/
d_JoinStrings
(Concatenating Lists into Narratives)
Just as threads are joined to form a tapestry, d_JoinStrings
allows you to concatenate an array of strings with a specified separator, mirroring Python's intuitive str.join()
method. This is invaluable for generating dynamic lists, file paths, or any delimited text, making complex string assembly a straightforward task.
/**
* @brief Join an array of strings with a separator (like Python's str.join()).
*
* @param sb Pointer to string builder.
* @param strings Array of string pointers to join.
* @param count Number of strings in the array.
* @param separator String to insert between each element.
*
* @note Does nothing if sb is NULL or count <= 0.
* @note If strings array is NULL, does nothing.
* @note NULL strings in the array are treated as empty strings.
* @note If separator is NULL, strings are joined without separation.
* @note Example: `d_JoinStrings(sb, {"a", "b", "c"}, 3, ", ")` produces `"a, b, c"`.
* @note Commonly used for creating comma-separated lists, file paths, etc.
*/
void d_JoinStrings(dString_t* sb, const char** strings, int count, const char* separator) {
if (sb == NULL || strings == NULL || count <= 0) return;
for (int i = 0; i < count; i++) {
if (strings[i] != NULL) {
d_AppendToString(sb, strings[i], 0);
}
// Add separator between elements (but not after the last one)
if (i < count - 1 && separator != NULL) {
d_AppendToString(sb, separator, 0);
}
}
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header
int main() {
dString_t* inventory_list = d_InitString();
if (inventory_list == NULL) {
fprintf(stderr, "Failed to initialize string builder!\n");
return 1;
}
const char* items[] = {"Sword of Minos", "Shield of Aegis", "Golden Fleece"};
d_JoinStrings(inventory_list, items, 3, ", ");
printf("Inventory: %s\n", d_PeekString(inventory_list));
// Expected Output: Inventory: Sword of Minos, Shield of Aegis, Golden Fleece
d_ClearString(inventory_list);
const char* quest_path[] = {"quests", "main", "labyrinth"};
d_JoinStrings(inventory_list, quest_path, 3, "/");
printf("Quest Path: %s\n", d_PeekString(inventory_list));
// Expected Output: Quest Path: quests/main/labyrinth
d_DestroyString(inventory_list);
return 0;
}
*/
d_SliceString
(Precise Extraction with Flexible Indices)
Extracting specific portions of a string is a common task in text processing. d_SliceString
provides Python-style slicing, allowing for intuitive positive and negative indexing to precisely extract substrings from an existing C-string. This offers a powerful and readable alternative to manual pointer arithmetic or strncpy
for substring extraction, embracing a more "Pythonic" approach to string manipulation.
/**
* @brief Extract a substring using Python-style slice notation (like str[start:end]).
*
* @param sb Pointer to string builder.
* @param text Source string to slice (must be null-terminated).
* @param start Starting index (inclusive, negative values count from end).
* @param end Ending index (exclusive, negative values count from end).
*
* @note Does nothing if sb or text is NULL.
* @note Negative indices count from the end: -1 is last character, -2 is second-to-last, etc.
* @note SPECIAL CASE: An 'end' value of -1 is treated as the end of the string.
* @note If start >= end (after resolving negative indices), no text is added.
* @note Indices are clamped to valid range [0, string_length].
* @note Example: `d_SliceString(sb, "Hello", 1, 4)` produces `"ell"`.
* @note Example: `d_SliceString(sb, "Hello", -3, -1)` produces `"llo"`.
*/
void d_SliceString(dString_t* sb, const char* text, int start, int end) {
if (sb == NULL || text == NULL) return;
int text_len = (int)strlen(text);
// Resolve negative indices for 'start'
if (start < 0) {
start = text_len + start;
}
// Resolve 'end' index, with a special case for -1 meaning "to the end"
if (end == -1) {
end = text_len;
} else if (end < 0) {
end = text_len + end;
}
// Clamp indices to a valid range to prevent out-of-bounds access
if (start < 0) start = 0;
if (start > text_len) start = text_len;
if (end < 0) end = 0;
if (end > text_len) end = text_len;
// After clamping, if the slice is invalid or empty, do nothing
if (start >= end) {
return;
}
// Calculate the length of the slice and ensure space in the builder
int slice_len = end - start;
d_StringBuilderEnsureSpace(sb, slice_len);
if (sb->str == NULL) return; // Check for realloc failure
// Copy the slice from the source text into the builder's buffer
// memcpy is used as we know the exact length and there's no overlap with source.
memcpy(sb->str + sb->len, text + start, slice_len);
sb->len += slice_len; // Update the builder's length
sb->str[sb->len] = '\0'; // Ensure null termination
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header
int main() {
dString_t* sliced_result = d_InitString();
if (sliced_result == NULL) {
fprintf(stderr, "Failed to initialize string builder!\n");
return 1;
}
const char* proverb = "Know thyself.";
d_SliceString(sliced_result, proverb, 0, 4); // "Know"
printf("Slice 1: '%s'\n", d_PeekString(sliced_result));
d_ClearString(sliced_result);
d_SliceString(sliced_result, proverb, -6, -1); // "yself."
printf("Slice 2: '%s'\n", d_PeekString(sliced_result));
d_ClearString(sliced_result);
d_SliceString(sliced_result, proverb, 5, 12); // "thyself"
printf("Slice 3: '%s'\n", d_PeekString(sliced_result));
d_DestroyString(sliced_result);
return 0;
}
*/
Robust Comparison with d_CompareStrings
For reliable string comparisons that respect the dString_t
's internal structure and length, d_CompareStrings
provides a safe and efficient lexicographical comparison. This function is crucial for sorting, searching, and validating string data, handling various edge cases gracefully, including invalid dString_t
pointers. It provides consistent and predictable results for string ordering.
/**
* @brief Helper to check if a dString_t object is invalid.
* @param sb Pointer to the dString_t object.
* @return True if the dString_t is NULL or its internal string pointer is NULL, false otherwise.
* @note This is typically a static helper in the .c file or a private utility.
*/
static bool d_IsStringInvalid(const dString_t* sb) {
return (sb == NULL || sb->str == NULL);
}
/**
* @brief Compare two dString_t objects lexicographically.
*
* @param str1 The first dString_t to compare.
* @param str2 The second dString_t to compare.
*
* @return int An integer less than, equal to, or greater than zero if `str1` is found,
* respectively, to be less than, to match, or be greater than `str2`.
* Handles invalid dString_t pointers gracefully.
*/
int d_CompareStrings(const dString_t* str1, const dString_t* str2)
{
// If both are invalid, they are considered equal (e.g., both NULL).
if (d_IsStringInvalid(str1) && d_IsStringInvalid(str2)) {
return 0;
}
// If str1 is invalid but str2 is valid, str1 is "less than" str2.
// This provides a consistent ordering for error states.
if (d_IsStringInvalid(str1)) {
return -1;
}
// If str2 is invalid but str1 is valid, str1 is "greater than" str2.
if (d_IsStringInvalid(str2)) {
return 1;
}
// If both are valid, perform length-aware binary comparison.
// First, compare the common prefix up to the length of the shorter string.
size_t min_len = (str1->len < str2->len) ? str1->len : str2->len;
int cmp = memcmp(str1->str, str2->str, min_len);
if (cmp != 0) {
return cmp; // If common prefix differs, return that difference.
}
// If common prefixes are identical, the shorter string is lexicographically "less than" the longer one.
if (str1->len != str2->len) {
return (str1->len < str2->len) ? -1 : 1;
}
// If both lengths and content are equal, the strings are identical.
return 0;
}
// Example Usage:
/*
#include <Daedalus.h> // Include your Daedalus header
int main() {
dString_t* s_alpha = d_InitString();
d_AppendToString(s_alpha, "alpha", 0);
dString_t* s_beta = d_InitString();
d_AppendToString(s_beta, "beta", 0);
dString_t* s_apple = d_InitString();
d_AppendToString(s_apple, "apple", 0);
dString_t* s_app = d_InitString();
d_AppendToString(s_app, "app", 0);
printf("'%s' vs '%s': %d (Expected 0)\n", d_PeekString(s_alpha), d_PeekString(s_apple), d_CompareStrings(s_alpha, s_apple));
printf("'%s' vs '%s': %d (Expected <0)\n", d_PeekString(s_alpha), d_PeekString(s_beta), d_CompareStrings(s_alpha, s_beta));
printf("'%s' vs '%s': %d (Expected >0)\n", d_PeekString(s_beta), d_PeekString(s_alpha), d_CompareStrings(s_beta, s_alpha));
printf("'%s' vs '%s': %d (Expected >0)\n", d_PeekString(s_alpha), d_PeekString(s_app), d_CompareStrings(s_alpha, s_app));
printf("'%s' vs '%s': %d (Expected <0)\n", d_PeekString(s_app), d_PeekString(s_alpha), d_CompareStrings(s_app, s_alpha));
d_DestroyString(s_alpha);
d_DestroyString(s_beta);
d_DestroyString(s_apple);
d_DestroyString(s_app);
return 0;
}
*/
Part 3: The Impact - What These Tools Enable
We've now explored the expanded capabilities of the Daedalus String Builder, adding powerful features like printf
-style formatting, dynamic UI element construction, and intuitive "Pythonic" string manipulations. But what does all this new functionality truly mean for our projects? In this section, we'll talk about the tangible benefits these advanced tools bring to the table. It's about how they empower us to write code that's not just functional, but also significantly cleaner, safer, and more efficient. Let's see how these new capabilities translate into real-world advantages for your development workflow.
Cleaner Code: Focus on Intent, Not Mechanics
Think back to the days of manually managing string buffers, calculating offsets, and constantly checking sizes. Your code would often be cluttered with low-level string mechanics, obscuring the actual logic you were trying to implement. It was like trying to write a novel while simultaneously forging every single pen and inkwell yourself.
With the advanced dString_t
functions, that burden is lifted. Our code becomes a direct expression of our intent.
- Before (manual formatting):
char buffer[256];
int current_pos = 0;
current_pos += snprintf(buffer + current_pos, sizeof(buffer) - current_pos, "Player: %s", player_name);
current_pos += snprintf(buffer + current_pos, sizeof(buffer) - current_pos, ", Score: %d", score);
// ... and so on for every piece ...
- After (
d_FormatString
):
dString_t* status_msg = d_InitString();
d_FormatString(status_msg, "Player: %s, Score: %d, Level: %d", player_name, score, level);
// ... much simpler, direct intent ...
d_DestroyString(status_msg);
By encapsulating complex operations like formatting, padding, and templating behind clear, single-purpose functions, our application logic becomes much easier to read and understand. You spend less time debugging string-related boilerplate and more time focusing on the unique challenges and creative aspects of your game or application. This clarity also makes your code more maintainable and easier for others (or your future self!) to pick up and contribute to.
Safer Code: Eliminating Entire Classes of Bugs
One of the most significant advantages of the dString_t
builder, and especially its advanced functions, is the dramatic improvement in code safety. C strings are notorious for leading to buffer overflows, memory leaks, and other hard-to-track bugs. Our string builder is designed from the ground up to prevent these issues.
-
No More Buffer Overflows (even with complex formatting): Functions like
d_FormatString
internally calculate the exact buffer size needed before writing any data. They then automatically expand thedString_t
's internal buffer if necessary. This means you can use complex format strings and variable arguments without ever worrying about writing past the end of your buffer. The same safety applies to padding, templating, and joining operations ā the builder always ensures sufficient space. -
Clear Memory Ownership: The
dString_t
library maintains a consistent and safe memory ownership model. Youd_InitString
to create the builder, and youd_DestroyString
to free it. When you need a temporary C-string pointer (e.g., forprintf
),d_PeekString
provides aconst
pointer, clearly indicating it's read-only and ephemeral. If you need a permanent, modifiable copy,d_DumpString
explicitly allocates new memory and makes you responsible for itsfree()
ing. This clear contract prevents common memory leaks and dangling pointer issues.
By handling memory management and bounds checking automatically and reliably, these advanced functions eliminate entire classes of dangerous bugs that plague traditional C string manipulation.
Faster Code: Efficiency Where It Counts
While safety and readability are crucial, performance is paramount in game development. The dString_t
builder's design, including these new advanced functions, prioritizes efficiency for the tasks they perform.
-
Amortized O(1) Appends: As discussed in previous parts, the core
d_AppendToString
(and by extension, functions that use it liked_FormatString
,d_RepeatString
,d_JoinStrings
, etc.) benefits from the "doubling strategy" for memory allocation. This means that whilerealloc
can be expensive, it happens infrequently. Over many appends, the average cost of adding characters is effectively constant time (amortized O(1)), which is significantly faster than the quadratic (O(n²)) performance of repeatedly usingstrcat
on a growing string. -
Optimized Operations: Functions like
d_RepeatString
use direct memory manipulation (for
loops writing characters directly) for maximum efficiency.d_FormatString
usesvsnprintf
twice, but this is a highly optimized standard library function, and the two-pass approach is the safest and most efficient way to handle dynamicprintf
-style formatting in C.d_SliceString
usesmemcpy
for fast block memory copies. -
Reduced Redundancy: By providing dedicated functions for common tasks (like padding or joining), you avoid reinventing the wheel with custom, potentially less optimized, and more bug-prone code each time you need that functionality. This leads to overall faster development and execution.
In essence, these advanced dString_t
functions are not just about convenience; they are about delivering high-performance, robust, and readable string handling capabilities that are essential for building demanding applications like game engines. They empower you to focus on the creative challenges of development, knowing that your string operations are handled with precision and care.
Part 4: The Ever-Evolving Library - Final Thoughts
We've now explored the expanded capabilities of the Daedalus String Builder. From its foundational principles of automatic memory management and safe appending, we've added powerful features like printf
-style formatting, dynamic UI element construction, and intuitive "Pythonic" string manipulations including templating, joining, and slicing. Each function is designed to be robust, efficient, and easy to use. These are the building blocks that empower developers to create complex narratives and intricate interfaces with careful engineering, unburdened by the common pitfalls of C string handling. This ongoing journey of discovery and creation is at the heart of open-source development.
The dString_t
object serves as a flexible foundation, and these new, advanced tools are the means by which we can build the rich world of ASCIIGame
. This expansion of the string library ensures that text, often a core component of an ASCII RPG, can be manipulated with unparalleled flexibility and safety. It's a testament to the idea that even in C, we can build high-level, intuitive abstractions that make development more enjoyable and less prone to errors.
The journey of Daedalus is one of continuous creation and refinement, a perpetual exploration of what is possible when passion meets practical engineering. As we continue to build the ASCIIGame
engine, further utilities will emerge, seamlessly integrating with the dString_t
foundation. Imagine sophisticated text parsing, rich text rendering with color and style codes, and even more advanced pattern matching ā all built upon the robust framework we've established. The beauty of open-source is that this library is ever-expanding, inviting new minds to contribute and discover.
Thank you for following along on this part of the journey. The road ahead is long, but with robust tools like this, I'm excited to see what we can build, together.
Stay tuned for more.
From the forge of necessity comes tools of transcendence. These functions will outlive their creator, serving digital architects for generations yet to come.