mls is a lightweight C library designed to provide the flexibility and ease of use found in interpreted languages while maintaining the performance and control of C. It features dynamic arrays (managed via handles), string manipulation, and "typeless" variables, all with built-in memory safety and optional automatic cleanup for nested structures.
- Handle-based Memory Management: Data structures are referenced by integer handles, reducing pointer errors and simplifying API calls.
- Dynamic Arrays: Automatically resizing arrays that can store any data type.
- Nested List Support: Built-in support for lists containing other lists, with safe recursive freeing (
MFREE_EACH). - UAF Protection: Basic Use-After-Free detection using protection patterns in handles.
- Debug Toolset: Integrated tracing and allocation tracking.
- No Dependencies: Standard C library only.
Simply include lib/mls.h in your project and link against lib/mls.c or the compiled object file.
# Typical compilation
gcc -I./lib your_program.c lib/mls.c -o your_programmls.h/c: The coremlslibrary and handle management.m_tool.h/c: Comprehensive string handling (s_printf,s_app,s_split,s_join), variables, and these_stringexpansion system.m_extra.h/c: Extended string utilities (case-insensitive comparison, padding, reversal, classification).m_table.h/c: A dictionary-like data structure for storing MLS handles, accessible by name or integer, with introspection.m_http.h/c,m_hdf.h/c,m_flask.h/c: Web and data format extensions.
The core of the library is the handle system. Instead of managing pointers to structures, you receive an int handle. All operations (adding, reading, deleting) are performed using this handle.
Beyond simple arrays, mls provides a powerful suite of string utilities:
s_printf(h, p, fmt, ...): Build strings with automatic resizing.se_string(vl, fmt): Modern string interpolation with variable expansion ($var,$var[index],$var[*]).m_extra: Case-insensitive comparisons, directional trimming, padding, and more.
Read the String Documentation and Expansion Documentation.
A handle is a 32-bit integer. The lower 24 bits represent the index in the global list of arrays, while the upper 8 bits are used for UAF (Use-After-Free) protection.
mls arrays are "width-aware". When you allocate a list, you specify the width of its elements (e.g., sizeof(int), sizeof(char*)). The library then handles the byte-level offsets for you.
In traditional C, managing dynamic arrays often involves using pointers (void*) or pointers-to-pointers (void**). mls uses integer handles to solve several common architectural issues:
When a dynamic array is resized (e.g., via realloc), its memory address might change. If you have multiple pointers to that array throughout your code, they all become "stale" (invalid) after the resize.
With handles, the actual memory address is managed internally in a central table. Your handle (the int) never changes, even if the underlying memory moves.
In standard C, if a function needs to resize a buffer passed by the caller, it must accept a pointer-to-pointer (void**) and often a pointer to the length. This makes code verbose and error-prone.
Traditional C approach:
// Caller must manage both the pointer and the length
size_t len = 128;
char *buf = malloc(len);
ssize_t bytes = read_line(&buf, &len, fp); // Must pass address of pointer
// ... later ...
free(buf);MLS approach:
// Caller only manages a single integer handle
int h = m_alloc(128, 1, MFREE);
int bytes = m_read_line(h, fp); // Handle stays the same even if resized
// ... later ...
m_free(h);By using handles, functions become much simpler to write and maintain because they don't need to "reach back" into the caller's memory to update pointers.
Creating a universal library in C usually requires extensive use of void*, which already bypasses compile-time type safety. By choosing int handles, mls trades this manual type management for significant runtime robustness:
- No Loss of Safety: Since generic C libraries already rely on
void*for "typeless" behavior, using aninthandle doesn't lose any safety that wasn't already being managed manually by the programmer. - Superior Validation: Unlike a raw pointer, which is just an address, an
inthandle can be rigorously validated. Every time you call anmlsfunction, the library checks if the handle is initialized, if it has been freed, and if its internal protection bits match. This prevents the most common C errors like double-frees or null-pointer dereferences. - Avoiding Workarounds: Strict typing for generic structures in C often leads to complex workarounds like "macro hell," nested unions, or repetitive boilerplate. Integer handles provide a clean, universal identifier that works identically across all data structures.
While handle-to-pointer resolution involves a small overhead compared to raw pointer access, this trade-off is negligible in most real-world scenarios:
- Safety at a Discount: A standard
if (ptr == NULL)check only verifies the address isn't zero. Themls()function performs a handle lookup, checks for initialization, and validates the UAF protection pattern in just a few extra CPU cycles. - Efficient Lookup: The internal handle-to-pointer mapping is implemented as a simple array index lookup. For performance-critical loops, you can always resolve the handle once and work with the resulting raw pointer directly.
- Value-Added Security: The minimal cost of handle resolution buys you features that would be much more expensive to implement manually, such as global Use-After-Free detection and cross-module memory tracking.
In mls, all valid handles are strictly positive integers (h > 0). This convention provides several benefits:
- Simple Initialization Checks: You can initialize your handle variables to
0and then use a simpleif (h > 0)check to verify if a list has been allocated. - Error Ambiguity Removal: Unlike pointers where only
NULL(0) is traditionally invalid,mlsexplicitly treats all values<= 0as unallocated or invalid handles. - Robust API: Functions like
m_free(h)gracefully ignore handles<= 0, preventing crashes if an uninitialized handle is passed. Core access functions likemls(h, i)strictly enforce theh > 0rule, ensuring that any attempt to access an invalid handle is caught immediately during development. (Note: While handles themselves must be positive, the data stored within an array—such as in a list of integers—can safely contain negative values.)
Fun Side Note: In a 64-bit world, a standard pointer takes up 8 bytes of memory. An
mlshandle is a lean 4-byte integer. If you find yourself storing the same reference multiple times (like in a complex graph), usingmlsis essentially putting your data structure on a half-calorie diet. Your cache will thank you!
The power of mls becomes even more apparent when building complex, nested data structures like trees, graphs, or JSON-like objects. By using integer handles for every child node instead of pointers, you can build deeply nested structures that are both robust and easy to manage.
Building a complex structure involves these core operations:
m_alloc(max, w, h): Creates a new list node.m_free(m): Safely deallocates a node and (ifMFREE_EACHis used) its entire subtree.mls(m, i): Returns avoid*to the element at indexiin handlem.
While mls() returns a generic void*, mls provides several macros for type-safe and concise data access:
| Macro | Type | Description |
|---|---|---|
STR(m, i) |
char* |
Access a string pointer at index i. |
INT(m, i) |
int |
Access an integer value at index i. |
CHAR(m, i) |
char |
Access a single character at index i. |
- Lists: Simply a handle where elements are data values (e.g.,
sizeof(int)). - Trees: A handle where each element is another handle (e.g.,
sizeof(int)withMFREE_EACH). - Graphs: Handles where elements represent edges (pointers to other handles). Because
m_freeuses the circular-reference marker (255), cycles in graphs are handled safely during recursion.
Building a tree node with multiple children is straightforward and avoids complex pointer arithmetic:
// Create a node that will contain child handles
int root = m_alloc(0, sizeof(int), MFREE_EACH);
// Create child nodes
int child1 = m_alloc(0, sizeof(int), MFREE_EACH);
int child2 = m_alloc(0, sizeof(int), MFREE_EACH);
// Add children to the root
m_put(root, &child1);
m_put(root, &child2);
// Recursive Cleanup:
// Because root was created with MFREE_EACH, a single call
// to m_free(root) will recursively free all children.
m_free(root);- Unified API: Use the same
m_put,mls, andm_freefunctions regardless of structural complexity. - Robust Code: No more dangling pointers or complex pointer arithmetic.
- Reduced Error Rate: Automated recursive freeing eliminates manual tracking of every child node, significantly reducing memory leak potential.
Allocates a new dynamic array.
max: Initial capacity (number of elements).w: Width of each element in bytes.free_hdl: A pre-registered free handler (e.g.,MFREE,MFREE_STR,MFREE_EACH).
Deprecated. Use m_alloc instead. Equivalent to m_alloc(max, w, MFREE).
Returns a pointer to the element at index i in handle m.
- Note: Always cast the result to your data type (e.g.,
*(int*)mls(h, 0)).
Appends data to the end of the array m. The library automatically resizes the buffer if needed.
- Returns the index of the newly added element.
A safer, handle-aware alternative to memcpy. Copies n elements from data into list m starting at pos.
- Note: It does not resize the buffer automatically. If you write beyond the current length, you should call
m_setlen(m, pos + n)to keep the list's size in sync.
Frees the memory associated with handle m. If a specialized free_hdl was used during allocation (like MFREE_STR), it will also free the elements (e.g., the strings inside the list) before freeing the list itself. It handles circular references safely.
#include "mls.h"
int main() {
m_init();
// Create a list for integers
int h = m_alloc(10, sizeof(int), MFREE);
for(int i = 0; i < 5; i++) {
m_put(h, &i);
}
printf("Value at index 2: %d\n", *(int*)mls(h, 2));
m_free(h);
m_destruct();
return 0;
}int h = m_alloc(5, sizeof(char*), MFREE_STR);
char *s = strdup("Hello World");
m_put(h, &s);
// m_free(h) will now automatically call free() on the string
m_free(h);The project uses a hierarchical Makefile system.
# Debug build (with MLS_DEBUG and trace support)
make
# Production build (optimized)
make production=1To run the test suite:
cd tests
makeThis will execute various tests including test_autofree, test_ssplit, and test_nested_lists.
Contributions are welcome! To contribute:
- Report Bugs: Use the issue tracker to report bugs or request features.
- Submit PRs: Ensure your code follows existing patterns and includes tests for any new functionality.
- Run Tests: Always run
make testbefore submitting changes.
This project is licensed under the MIT License. See the LICENSE file for details.