<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Yair Lenga on Medium]]></title>
        <description><![CDATA[Stories by Yair Lenga on Medium]]></description>
        <link>https://medium.com/@yair.lenga?source=rss-2c0c4950e7e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*RgOSV-JrxwCMUxwU8gcVZg.jpeg</url>
            <title>Stories by Yair Lenga on Medium</title>
            <link>https://medium.com/@yair.lenga?source=rss-2c0c4950e7e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 24 May 2026 02:25:15 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@yair.lenga/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[A Streaming JSON Formatter That Works With Existing Serializers]]></title>
            <link>https://medium.com/@yair.lenga/a-streaming-json-formatter-that-works-with-existing-serializers-eced220da37d?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/eced220da37d</guid>
            <category><![CDATA[json]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Wed, 20 May 2026 20:46:00 GMT</pubDate>
            <atom:updated>2026-05-21T08:25:52.160Z</atom:updated>
            <content:encoded><![CDATA[<p>A Python streaming post-filter for compact, human-readable JSON with configurable formatting behavior.</p><p>Built-in JSON serializers give us two choices:</p><p>The default output is built for machines and optimized for efficiency. It is compact, without any extra whitespace. While technically “text”, it feels “binary” — a dense wall of brackets, quotes, commas, and braces that is painful to inspect.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NuclDLhpuF1iLLHSK0Qs-Q.png" /></figure><p>To solve this problem many serializers provide a “Pretty-print” mode, which adds indentation, spacing around tokens and line breaks — making it readable for humans. The problem is that for large documents it often goes too far: A small array of numbers becomes ten lines. A tiny metadata object becomes a block. Deep structures become readable only by making the file much longer.</p><p>That extra formatting is not free. It makes logs larger, diffs noisier, terminal output harder to scan, and requires “speed-scrolling” to review the data sets.</p><p>What I wanted was a middle ground: JSON that keeps the shape of pretty-printed output, but folds small, simple structures back onto one line when they fit.</p><p>This article describes jsonfold, a process for &quot;compacting&quot; pretty-print JSON data to make it more readable for humans. The level of &quot;compactness&quot; is controlled by parameters, and there are a few preset configurations that can be used to get output with minimal effort.</p><h3>jsonfold in 2 minutes</h3><blockquote><em>Get fine control over the pretty-print JSON output. Keep it machine-readable and human-friendly.</em></blockquote><h4>Minimal Usage</h4><p>Pull jsonfold.py from <a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-files/jsonfold.py">GitHub project</a></p><pre>import jsonfold<br>import sys<br>data = {<br>    &quot;meta&quot;: {&quot;version&quot;: 1, &quot;ok&quot;: True},<br>    &quot;ids&quot;: [1, 2, 3, 4, 5],<br>    &quot;items&quot;: [{&quot;id&quot;: 1, &quot;name&quot;: &quot;alpha&quot;}, {&quot;id&quot;: 2, &quot;name&quot;: &quot;beta&quot;}],<br>}<br># compact can be: default, low, med, high, max<br>jsonfold.dump(data, sys.stdout, compact=&quot;default&quot;)</pre><h4>GitHub Project</h4><p>Repository: <a href="https://github.com/yairlenga/jsonfold">https://github.com/yairlenga/jsonfold</a></p><p>Python implementation is under python directory.</p><h4>Different levels of compaction</h4><pre>// compact=low<br>{<br>  &quot;a&quot;: {<br>    &quot;b&quot;: { &quot;c&quot;: &quot;abc&quot; }<br>  },<br>  &quot;x&quot;: {<br>    &quot;y&quot;: { &quot;z&quot;: &quot;xyz&quot; }<br>  }<br>}</pre><pre>// Compact=default<br>{<br>  &quot;a&quot;: { &quot;b&quot;: { &quot;c&quot;: &quot;abc&quot; } },<br>  &quot;x&quot;: { &quot;y&quot;: { &quot;z&quot;: &quot;xyz&quot; } }<br>}</pre><pre>// Compact=max<br>{ &quot;a&quot;: { &quot;b&quot;: { &quot;c&quot;: &quot;abc&quot; } }, &quot;x&quot;: { &quot;y&quot;: { &quot;z&quot;: &quot;xyz&quot; } } }</pre><h3>jsonfold on real data.</h3><p>Using the geojson file <a href="https://geojson.xyz/">geojson.xyz: admin 1 states provinces shp</a>. You can view the actual output:</p><ul><li><a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-python/geojson.json">Baseline — no formatting</a>: 130K, 1 line, 130,429 columns, 0% overhead</li><li><a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-python/geojson-none.json">Pretty-Printed, indent=2</a>: 285K, 11731 lines, 79 columns, 120% overhead</li><li><a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-python/geojson-low.json">jsonfold compact=low</a>: 167K, 2344 lines, 120 columns, 28% overhead</li><li><a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-python/geojson-default.json">jsonfold compact=default</a>: 167K, 2344 lines, 120 columns, 28% overhead</li><li><a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-python/geojson-high.json">jsonfold compact=high</a>: 166K, 2239 lines, 120 columns, 27% overhead</li><li><a href="https://raw.githubusercontent.com/yairlenga/jsonfold/refs/heads/main/articles/01-python/geojson-max.json">jsonfold compact=max</a>: 156K, 1321 lines, 255 columns, 20% overhead</li></ul><h3>Key ideas</h3><h4>Do not replace the serializer</h4><p>For my implementation, I chose NOT to build another serializer. There are already many good serializers available in multiple languages. Many of them support custom transformations, special handling for application classes, non-standard numeric values, date/time objects, and other data types. For example:</p><ul><li>The Python <a href="https://docs.python.org/3/library/json.html">json module — JSON encoder and decoder</a> provides options for custom data types, NaN handling, custom encoders, and more.</li><li>JavaScript’s <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify">JSON.stringify()</a> supports a replacer function that can alter the stringification process.</li><li><a href="https://github.com/fasterxml/jackson">Java Jackson </a>ObjectMapper can perform complex transformation of POJOs based on annotations, introspection and templates.</li></ul><h4>Wrap the output stream</h4><p>Instead of replacing or extending the serializer, jsonfold acts as a filter between the serializer and the final output stream.</p><p>The serializer still does what it already knows how to do: convert application data into valid JSON text. jsonfold only looks at the generated pretty-printed output and decides which parts can be safely folded back onto one line.</p><p>This keeps all the existing serializer functionality and customization in place.</p><h4>Operate on pretty-printed token stream</h4><p>The jsonfold does not reparse the JSON format or reconstruct the data objects. Instead, it operates directly on the pretty-printed token stream generated by the serializer. It relies on a few assumptions:</p><ol><li>The input is valid JSON.</li><li>Each input line represents an atomic JSON fragment that can be safely moved or merged as a unit, and should NOT be split or reformatted.</li><li>Indentation provides structural clues about the relationship between elements.</li></ol><p>If the above assumptions are violated, the jsonfold falls back into &quot;raw&quot; mode - where the data is passed through unchanged, without attempting any unsafe transformation.</p><h3>The three phases</h3><p>The compaction is done in three logical phases, we will name them: <strong>pack</strong>, <strong>fold</strong> and <strong>join</strong>. Each one performs a specific transformation that makes the JSON easier to read (by removing whitespace), while not changing the data itself. Separating the process into phases makes the implementation simpler and more predictable. Each phase operates on progressively more compact structures while preserving the original JSON semantics. All three phases are incremental, and process the stream as data becomes available.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/718/1*OPRFRKQS3MWDLMVNOAdstw.png" /></figure><h4>Pack</h4><p>The <strong>pack</strong> phase handles merging of scalar items inside containers (array, object). It will “pack” array items and object properties that belong to the same containers into single line, subject to specific width, and limits. Basically:</p><pre>// From            To:<br>[                  [<br>    &quot;1&quot;,<br>    &quot;2&quot;,      -&gt;      &quot;1&quot;, &quot;2&quot;, &quot;3&quot;<br>    &quot;3&quot;<br>]                  ]<br><br>{                  {<br>    &quot;a&quot;: 1,<br>    &quot;b&quot;: 2,   -&gt;      &quot;a&quot;: 1, &quot;b&quot;: 2, &quot;c&quot;: 3<br>    &quot;c&quot;: 3<br>}                  }</pre><p>Example:</p><pre>{<br>    &quot;summary&quot;: {<br>        &quot;source&quot;: &quot;wikipedia&quot;<br>    },<br>    &quot;meta&quot;: {<br>        &quot;generated&quot;: &quot;2026-03-13&quot;<br>    },<br>    &quot;by_land&quot;: [<br>        &quot;RUS&quot;,<br>        // 4 more entries<br>        &quot;AUS&quot;<br>    ],<br>    &quot;by_population&quot;: [<br>        &quot;IND&quot;,<br>        &quot;CHN&quot;,<br>        // 16 Additional entries<br>        &quot;DEU&quot;,<br>        &quot;TZA&quot;<br>    ]<br>    &quot;name&quot;: {<br>        &quot;RUS&quot;: &quot;Russia&quot;,<br>        &quot;CAN&quot;: &quot;Canada&quot;,<br>        &quot;CHN&quot;: &quot;China&quot;,<br>        &quot;USA&quot;: &quot;United States&quot;,<br>        &quot;BRA&quot;: &quot;Brazil&quot;,<br>        &quot;AUS&quot;: &quot;Australia&quot;<br>    }<br>}</pre><p>to:</p><pre>{<br>  &quot;summary&quot;: { &quot;source&quot;: &quot;wikipedia&quot; },<br>  &quot;meta&quot;: { &quot;generated&quot;: &quot;2026-03-13&quot; },<br>  &quot;by_land&quot;: [<br>    &quot;RUS&quot;, &quot;CAN&quot;, &quot;CHN&quot;, &quot;USA&quot;, &quot;BRA&quot;, &quot;AUS&quot;<br>  ],<br>  &quot;by_population&quot;: [<br>    &quot;IND&quot;, &quot;CHN&quot;, &quot;USA&quot;, &quot;IDN&quot;, &quot;PAK&quot;, &quot;NGA&quot;, &quot;BRA&quot;, &quot;BGD&quot;, &quot;RUS&quot;,<br>    &quot;ETH&quot;, &quot;MEX&quot;, &quot;JPN&quot;, &quot;EGY&quot;, &quot;PHL&quot;, &quot;COD&quot;, &quot;VNM&quot;, &quot;IRN&quot;, &quot;TUR&quot;,<br>    &quot;DEU&quot;, &quot;TZA&quot;<br>  ],<br>  &quot;name&quot;: {<br>    &quot;RUS&quot;: &quot;Russia&quot;, &quot;CAN&quot;: &quot;Canada&quot;, &quot;CHN&quot;: &quot;China&quot;,<br>    &quot;USA&quot;: &quot;United States&quot;, &quot;BRA&quot;: &quot;Brazil&quot;, &quot;AUS&quot;: &quot;Australia&quot;<br>  }<br>}</pre><p>More technically: The first phase looks for lines with the same indentation level, and will merge consecutive lines in such a way that it will respect the user provided line width. In addition, it is possible to cap the count of lines that will be packed for arrays and for objects.</p><h4>Fold</h4><p>The <strong>Fold</strong> phase handles merging of containers that have only one line of items with the container opener/closer (For arrays: [ and ], for objects: {, }), subject to specific width, nesting level and item counts. Basically:</p><pre>// List Folding: From 3 lines <br>[<br>    &quot;1&quot;, &quot;2&quot;, &quot;3&quot;<br>]<br>//     To: single line<br>[ &quot;1&quot;, &quot;2&quot;, &quot;3&quot; ]<br><br>// Object Folding: From 3 lines:<br>{<br>    &quot;a&quot;: 1, &quot;b&quot;: 2, &quot;c&quot;: 3<br>}<br>//     To: single line<br>{ &quot;a&quot;: 1, &quot;b&quot;: 2, &quot;c&quot;: 3 }</pre><p>Continuing with the above example, the attributes ‘by_land’ and ‘summary’ are now shown in a single line.</p><pre>{<br>  &quot;summary&quot;: { &quot;source&quot;: &quot;wikipedia&quot; },<br>  &quot;meta&quot;: { &quot;generated&quot;: &quot;2026-03-13&quot; },<br>  &quot;by_land&quot;: [ &quot;RUS&quot;, &quot;CAN&quot;, &quot;CHN&quot;, &quot;USA&quot;, &quot;BRA&quot;, &quot;AUS&quot; ],<br>  &quot;by_population&quot;: [<br>    &quot;IND&quot;, &quot;CHN&quot;, &quot;USA&quot;, &quot;IDN&quot;, &quot;PAK&quot;, &quot;NGA&quot;, &quot;BRA&quot;, &quot;BGD&quot;,<br>    &quot;RUS&quot;, &quot;ETH&quot;, &quot;MEX&quot;, &quot;JPN&quot;, &quot;EGY&quot;, &quot;PHL&quot;, &quot;COD&quot;, &quot;VNM&quot;,<br>    &quot;IRN&quot;, &quot;TUR&quot;, &quot;DEU&quot;, &quot;TZA&quot;<br>  ],<br>  &quot;name&quot;: {<br>    &quot;RUS&quot;: &quot;Russia&quot;, &quot;CAN&quot;: &quot;Canada&quot;, &quot;CHN&quot;: &quot;China&quot;, &quot;USA&quot;: &quot;United States&quot;,<br>    &quot;BRA&quot;: &quot;Brazil&quot;, &quot;AUS&quot;: &quot;Australia&quot;<br>  }<br>}</pre><h4>Join</h4><p>The <strong>join</strong> phase is similar to the <strong>pack</strong> phase — it will attempt to merge folded lines together, potentially merging folded objects into the same line, subject to specific width, nesting level and item counts.</p><p>At this stage, previously folded containers are treated as atomic units that can be merged together while preserving their internal structure. This allows nested structures such as coordinate pairs or small embedded objects to behave similarly to scalar values during compaction.</p><p>Continuing with the above example, the attributes ‘summary’ and ‘meta’ are now merged into a single line.</p><pre>{<br>  // summary and meta merge into a single line.  <br>  &quot;summary&quot;: { &quot;source&quot;: &quot;wikipedia&quot; }, &quot;meta&quot;: { &quot;generated&quot;: &quot;2026-03-13&quot; },<br>  &quot;by_land&quot;: [ &quot;RUS&quot;, &quot;CAN&quot;, &quot;CHN&quot;, &quot;USA&quot;, &quot;BRA&quot;, &quot;AUS&quot; ],<br>  &quot;by_population&quot;: [<br>    &quot;IND&quot;, &quot;CHN&quot;, &quot;USA&quot;, &quot;IDN&quot;, &quot;PAK&quot;, &quot;NGA&quot;, &quot;BRA&quot;, &quot;BGD&quot;,<br>    &quot;RUS&quot;, &quot;ETH&quot;, &quot;MEX&quot;, &quot;JPN&quot;, &quot;EGY&quot;, &quot;PHL&quot;, &quot;COD&quot;, &quot;VNM&quot;,<br>    &quot;IRN&quot;, &quot;TUR&quot;, &quot;DEU&quot;, &quot;TZA&quot;<br>  ],<br>  &quot;name&quot;: {<br>    &quot;RUS&quot;: &quot;Russia&quot;, &quot;CAN&quot;: &quot;Canada&quot;, &quot;CHN&quot;: &quot;China&quot;, &quot;USA&quot;: &quot;United States&quot;,<br>    &quot;BRA&quot;: &quot;Brazil&quot;, &quot;AUS&quot;: &quot;Australia&quot;<br>  }<br>}</pre><h3>Why streaming matters</h3><p>JSON documents can be very large and deeply nested. It’s easier to implement the compaction by operating on a complete pretty-printed JSON document — but this has a price:</p><ul><li>Additional memory — having to hold both the original document and the compacted document can increase temporary memory usage to 2–4 times the size of the original document.</li><li>Operations on large strings: Concatenation and iteration over large strings are more costly than operations on smaller chunks.</li><li>Time to first byte (“ttfb”): delaying processing until the full documents is generated means that ttfb increases significantly. This can have noticeable negative impact on the service responsiveness to end users.</li></ul><p>The jsonfold processes the document in small bites - leveraging the incremental generation provided by the python json.dump() call - arrays are processed one item at a time, and objects are processed one key/value pair at a time. The extra memory that is needed for processing is approximately 4X the maximum width (actual or set).</p><p>If the string generation call json.dumps() is being used - there is no choice but to build and return the (potentially huge) final string. In this case, the incremental processing will cap the amount of extra memory as described above, and io.StringIO() to build the final string reduces the cost.</p><p>One important advantage of the streaming approach is that it should work with any other encoders (parameter cls in json.dump) and pretty-printers that can send output directly to a file-like object, by wrapping the existing file-like output stream with the jsonfold JSONFoldWriter class. (disclaimer: I do not use any third-party libraries, did not test any specific package).</p><p>Example: using custom encoder</p><pre>import json<br>import jsonfold<br># Custom object<br>class Foo:<br>    def __init__(self, name):<br>        self.name = name<br><br># Custom encoder for Person objects<br>class CustomEncoder(json.JSONEncoder):<br>    def default(self, obj):<br>        if isinstance(obj, Foo):<br>            return {&quot;ID&quot;: obj.name}<br>        return super().default(obj)<br><br># Sample custom object<br>foo_obj = Foo(&quot;Bar&quot;)<br># Encoding custom object using the custom encoder<br>jsonfold.dump(foo_obj, cls=CustomEncoder)</pre><p>Or using different serializer like rapidjson</p><pre>import rapidjson<br>import jsonfold<br>import sys<br><br>def pp(obj, fp, *,<br>         compact =  &quot;&quot;,<br>         indent: int = 2, **kwargs) -&gt; None:<br><br>    with jsonfold.JSONFoldWriter(fp, compact=compact) as out:<br>        rapidjson.dump(obj, out, indent=indent, **kwargs)</pre><h3>Cross-language portability</h3><p>This article covers the jsonfold implementation in python - the same approach can be used in other languages to format JSON according to the same rules - leveraging existing JSON serializers, and various stream filtering in other languages.</p><ul><li>Javascript: In Node, the Writable stream can be wrapped to apply the jsonfold logic on the output from any JSON serializer.</li><li>In Java, the java.io.FilterWriter can be used to add jsonfold formatting to any character stream.</li><li>In C, the FILE * object can be customized using the GLIBC extension fopencookie or BSD funopen</li></ul><p>Future articles will describe implementations in JavaScript, Java, C and other languages/platforms. Each implementation will be:</p><ul><li>Single file that can be dropped into the code base (Note: certain languages need separate header file).</li><li>Filter that will attach the jsonfold behavior to existing output stream.</li><li>Efficient implementation that minimizes memory usage, and overhead.</li><li>Self-contained, and does not introduce additional dependencies.</li></ul><h3>Limitations</h3><p>Reiterating the limitations of this approach: The jsonfold depends on the structure produced by a normal pretty-printer. jsonfold is not a general JSON parser, and it does not try to understand arbitrary JSON text.</p><p>In particular:</p><ul><li>The input must already be valid JSON.</li><li>The input should use a regular pretty-printed layout.</li><li>Indentation must reflect the nesting structure.</li><li>Each input line is expected to represent an atomic JSON fragment.</li><li>The formatter is not designed to recover from malformed JSON.</li><li>Highly customized pretty-printers may produce layouts that cannot be safely compacted.</li></ul><p>When these assumptions are violated, jsonfold falls back to pass-through mode rather than risking an unsafe transformation.</p><p>This also means that jsonfold is best used as a post-filter for trusted serializer output, not as a cleanup tool for arbitrary JSON pasted from unknown sources.</p><h3>Disclaimer</h3><p>The examples and benchmarks in this article, including linked code snippets, are simplified and reconstructed for illustration purposes. They are not taken from any production system, and do not reflect the design or implementation of any specific codebase.</p><p>This is a personal approach based on general experience working with codebases. It does not represent any official guideline or the opinion of my employer.</p><p>As always, evaluate and test the code carefully before adopting it in production.</p><h3>Usage and License</h3><p>The supporting file (jsonfold.py, and json examples) are provided under the MIT license and are intended to be copied and used as-is in your own projects.</p><p>You can simply copy and/or modify them into your project and integrate those files into your build process — no special packaging or setup is required</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eced220da37d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop Waiting for defer: A Practical Cleanup Layer for C]]></title>
            <link>https://blog.stackademic.com/stop-waiting-for-defer-a-practical-cleanup-layer-for-c-10bd03c09d64?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/10bd03c09d64</guid>
            <category><![CDATA[low-level-programming]]></category>
            <category><![CDATA[c-language]]></category>
            <category><![CDATA[reliability]]></category>
            <category><![CDATA[gcc]]></category>
            <category><![CDATA[clang]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Thu, 07 May 2026 14:18:32 GMT</pubDate>
            <atom:updated>2026-05-08T09:09:22.726Z</atom:updated>
            <content:encoded><![CDATA[<h3>Not another macro trick. Just a small set of cleanup helpers that cover memory, files, descriptors, sockets, and custom objects.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FqKmV-GB3g4bxmyQXbOJpQ.png" /><figcaption>Do Not Wait for DEFER</figcaption></figure><h3>Introduction</h3><p>I recently came across a post on Reddit: “I’m tired of waiting for the C language to finish specifying the defer function. What can I do?”</p><p>If you do a quick search, there is no shortage of answers. Over the years, many developers have built defer emulation in C - some clever, some portable, some tricky, and some break on newer compilers. The problem is not lack of ideas, but that many of them are not something you would standardize across a real codebase. (For a survey of implementations, see <a href="https://antonz.org/defer-in-c/">(Un)portable defer in C</a></p><p>My article is <strong>not</strong> about another attempt to implement defer, describe what you can do with it, compare it to other languages, or debate design choices. My goal here is much simpler: define a small, consistent set of cleanup macros that we can safely use in day-to-day C code. In practice, this approach eliminates most repetitive cleanup code in typical C functions, reduces error-handling boilerplate, and makes resource management predictable across the codebase.</p><p>The initial implementation will use compiler extensions. When a standard defer becomes available, these macros can be adapted to use it.</p><p>This approach is especially relevant for projects that run across multiple environments. In many cases older environments must be supported for years, which makes adapting new language features like defer a long-term process. Many legacy projects are also 5-10 years behind the current technology stack - which means they will not be able to leverage new defer anytime soon.</p><h4>What this gives you</h4><ul><li>Uniform cleanup patterns across your codebase</li><li>Fewer leaks and double-free bugs</li><li>Cleaner error handling paths (especially with early returns)</li><li>No dependency on future language features</li></ul><h4>Example</h4><p>The following patterns already cover most real-world resource management in typical C codebases:</p><pre>// calloc -&gt; free<br>char *p = calloc(n, sizeof(*p)) ;<br>if ( !p ) ... ;<br>DEFER_FREE(p) ;<br><br>// fopen -&gt; fclose<br>FILE *fp = fopen(filename, &quot;r&quot;) ;<br>if ( ! fp ) ... ;<br>DEFER_FCLOSE(fp) ;<br>// Work file -&gt; remove file<br>FILE *new_file = fopen(workfile, &quot;w&quot;) ;<br>DEFER_REMOVE(workfile) ;<br>if ( ! new_file ) ... ;<br>DEFER_FCLOSE(new_file) ;</pre><p>These macros follow a simple pattern: free, close, destroy, or restore. Once those are standardized, most cleanup becomes predictable.</p><p>If your codebase works with lower level objects (file descriptors, sockets, mutexes, …), a few additional macros will be useful:</p><pre>// open -&gt; close<br>int fd = open(filename, O_RDONLY) ;<br>if ( fd &lt; 0 ) ... ;<br>DEFER_FD_CLOSE(fd) ;<br><br>// Socket -&gt; shutdown/close<br>int sock = socket(AF_INET, SOCK_STREAM, 0) ;<br>if ( sock &lt; 0 ) ... ;<br>DEFER_FD_CLOSE(sock) ;<br>if ( connect(...) ) ... ;<br>DEFER_SOCK_SHUTDOWN(sock, SHUT_RDWR) ;<br>// Mutex lock<br>pthread_mutex_lock(&amp;lock) ;<br>DEFER_MUTEX_UNLOCK(&amp;lock) ;</pre><h4>Usage and License</h4><p>The supporting files (defer_call.c, defer_call.h) are provided under the MIT license and are intended to be copied and used as-is in your own projects.</p><p>You can simply copy and/or modify them into your project and integrate them into your build process — no special packaging or setup is required</p><ul><li>Header file: <a href="https://gist.github.com/yairlenga/e17feeb2709e5174630c277f444d7b85">defer_call.h</a></li><li>Helper code: <a href="https://gist.github.com/yairlenga/4967c9e3288cf258bd967eb5d74c0c09">defer_call.c</a></li><li>GitHub Repo: (including test cases): <a href="https://github.com/yairlenga/articles/blob/main/2026-defer-now/">https://github.com/yairlenga/articles/blob/main/2026-defer-now/</a></li></ul><h3>Inventory of provided macros</h3><p>This project is intentionally small.</p><p>The goal is not to introduce a new abstraction layer, but to provide a set of useful macros that cover the common resource-cleanup patterns in C using DEFER-like macros.</p><p>The cleanup function is invoked with the value of the resource identifier at the end of the block.</p><blockquote><em>Rule: If the resource is released in the middle of the block — it is important to set its identifier to NULL (or other invalid value like -1) to prevent double-cleanup.</em></blockquote><p>The core list covers the most common resource types.</p><ul><li>DEFER_FREE(void *p) for heap allocated memory</li><li>DEFER_FCLOSE(FILE *fp) for FILE * streams</li><li>DEFER_FD_CLOSE(int fd) for file descriptors</li><li>DEFER_DESTROY(void (*fn)(void *p), void *p) for user defined objects.</li></ul><p>The full list (categories) includes</p><p>Memory:</p><ul><li>DEFER_FREE(void *p) — Core</li><li>DEFER_FREE_PTR_ARRAY(void **a, int sz) — free list of pointers.</li></ul><p>Files:</p><ul><li>DEFER_FCLOSE(FILE *fp) — core</li><li>DEFER_REMOVE(const char *p)</li><li>DEFER_PCLOSE(FILE *fp)</li><li>DEFER_CLOSEDIR(DIR *d)</li></ul><p>File Descriptors, Sockets:</p><ul><li>DEFER_FD_CLOSE(int fd) — core</li><li>DEFER_SOCK_SHUTDOWN(int fd, int how)</li></ul><p>Synchronization:</p><ul><li>DEFER_MUTEX_UNLOCK(pthread_mutex_t *m)</li><li>DEFER_RWLOCK_UNLOCK(pthread_rwlock_t *lock)</li></ul><p>Using user defined destroy functions:</p><ul><li>DEFER_DESTROY(void (*fn)(void *p), void *p) for user defined objects — core.</li><li>DEFER_DESTROY_M(void (*fn)(void *p, int mode), void *p, int mode) to pass a mode parameter.</li><li>DEFER_DESTROY_X(void (*fn)(void *p, void *cxt), void *p, void *cxt) for user defined objects.</li></ul><h4>CORE: DEFER_FREE(void *ptr)</h4><p>To prevent memory leak — add DEFER_FREE after allocating the block with malloc, calloc, realloc(NULL, ...) or similar. The pattern covers resizing with realloc, reallocarray or similar functions, as long as resized address is stored to the same variable.</p><pre>{<br>    char *cp = malloc(n) ;<br>    if ( !cp ) return ERROR ;<br>    DEFER_FREE(cp) ;<br>    ...<br>    cp = realloc(cp, n + 100) ;<br>    ...<br>}</pre><p>If the allocated memory can be freed before the end of the block, the resource identifier must be set to NULL.</p><pre>{<br>    char *cp = calloc(n, sizeof(*cp)) ;<br>    if ( !cp ) return ERROR ;<br>    DEFER_FREE(cp) ;<br>    ...<br>    free(cp) ; <br>    cp = NULL ;<br>}</pre><h4>CORE: DEFER_FCLOSE(FILE *fp)</h4><p>To prevent leakage of FILE * object, DEFER_FCLOSE can be called after functions that create FILE * - fopen, fdopen, freopen.</p><pre>{<br>    FILE *fp = fopen(filename, &quot;r&quot;) ;<br>    if ( !fp ) return ERROR ;<br>    DEFER_FCLOSE(fp) ;<br>}</pre><p>If the file is closed before the end of the block, the resource identifier must be set to NULL. At that point, it can even be reused.</p><pre>{<br>    FILE *fp = fopen(filename, &quot;r&quot;) ;<br>    if ( !fp ) return ERROR ;<br>    DEFER_FCLOSE(fp) ;<br>    ...<br>    <br>    ...<br>    fclose(fp) ;<br>    fp = NULL ;<br>    ...<br>    fp = fopen(filename2, &quot;r&quot;) ;<br>    if ( !fp ) return ERROR ;<br>    ...<br>}</pre><h4>CORE: DEFER_FD_CLOSE(int fd)</h4><p>To prevent leakage of file descriptors, DEFER_FD_CLOSE can be called after any function that creates a file descriptor (open, creat, socket, dup, ...).</p><pre>{<br>    int fd = open(filename, O_RDONLY) ;<br>    DEFER_FD_CLOSE(fd) ;<br>    if ( fd &lt; 0 ) return ERROR ;<br>    ...<br>}</pre><p>If the file descriptor is explicitly closed in the block it is important to set the resource identifier to -1. Possible to set the resource identifier even before the first call.</p><pre>{<br>    int fd = -1 ;<br>    DEFER_FD_CLOSE(fd) ;<br>    ...<br>    fd = open(filename, O_RDONLY) ;<br>    if ( fd &lt; 0 ) return ;<br>    ...<br>    close(fd) ;<br>    fd = -1 ;<br>    ...<br>    fd = open(file2, O_RDONLY) ;<br>    if ( fd &lt; 0 ) return ;<br>}</pre><h4>MEMORY: DEFER_FREE_PTR_ARRAY(void **a, int sz)</h4><p>One common use case for managing list of large objects is to track list of pointers to created objects inside a fixed-size, or dynamic array of pointers. The DEFER_FREE_PTR_ARRAY can be used to call free on each element. The DEFER_FREE_PTR_ARRAY does not free the array itself — which should be registered with DEFER_FREE(a) when it is allocated dynamically.</p><pre>struct foo { ... }<br><br>{<br>    struct foo **list = NULL ;<br>    DEFER_FREE(list) ;<br>    int pos = 0 ;<br>    DEFER_FREE_PTR_ARRAY(list, pos) ;<br>    ...<br>    for (...) {<br>        list = realloc(list, (pos+1)*sizeof(*list)) ;<br>        list[pos] = calloc(1, sizeof(*list[pos])) ;<br>        pos++ ;<br>        ...<br>    }<br>}</pre><h4>FILES: DEFER_REMOVE(const char *pathname)</h4><p>When creating work files, it might be useful to remove the work file in addition to closing the FILE * object. This will ensure work files are removed when no longer needed.</p><pre>{<br>    FILE *fp = fopen(workfile, &quot;w&quot;) ;<br>    DEFER_REMOVE(workfile) ;<br>    if ( !fp ) return ERROR ;<br>    DEFER_FCLOSE(fp) ;    <br>}</pre><h4>FILES: DEFER_PCLOSE(FILE *fp)</h4><pre>{<br>    FILE *fp = popen(&quot;ls&quot;, &quot;r&quot;) ;<br>    DEFER_PCLOSE(fp) ;<br>    ...    <br>}</pre><h4>FILES: DEFER_CLOSEDIR(DIR *dirp)</h4><pre>{<br>    DIR *dir = opendir(dir_path) ;<br>    if ( !dir ) return ERROR ;<br>    DEFER_CLOSEDIR(dir) ;<br>}</pre><h4>SOCKET: DEFER_SOCK_SHUTDOWN(int sock, int how)</h4><p>Managing sockets requires executing shutdown once the socket has been connected (or after accept).</p><pre>{<br>    int sock = socket(...) ;<br>    if ( sock &lt; 0 ) return ERROR ;<br>    DEFER_FD_CLOSE(sock) ;<br>    ...<br>    // Shutdown required only after connect<br>    if ( connect(sock, ...) &lt; 0 ) return ERROR ;<br>    DEFER_SOCK_SHUTDOWN(sock, SHUT_RDWR) ;<br>    ...<br>}</pre><h4>Synchronization: DEFER_MUTEX_UNLOCK(pthread_mutex_t *m)</h4><pre>pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER ;<br><br>{<br>    pthread_mutex_lock(&amp;m) ;<br>    DEFER_MUTEX_UNLOCK(&amp;m) ;<br>}</pre><p>If the lock is released before the exit, the resource should be set to NULL to avoid double-cleanup</p><pre>pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER ;<br><br>{<br>    pthread_mutex_t *mp = &amp;my_mutex ;<br>    pthread_mutex_lock(mp) ;<br>    DEFER_MUTEX_UNLOCK(mp) ;<br>    ...<br>    // early release of the lock:<br>    pthread_mutex_unlock(mp) ;<br>    mp = NULL ;<br>}</pre><h4>Synchronization: DEFER_RWLOCK_UNLOCK(pthread_rwlock_t *lock)</h4><pre>pthread_rwlock_t  my_lock = PTHREAD_RWLOCK_INITIALIZER ;<br><br>{<br>    pthread_rwlock_t *lp = &amp;my_lock ;<br>    // Read some data<br>    pthread_rwlock_rdlock(lp) ;<br>    DEFER_RWLOCK_UNLOCK(lp) ;<br>    ...<br>    // Release<br>    pthread_rwlock_unlock(lp) ;<br>    lp = NULL ;<br>    ...<br>    // Write some data, using the same lock<br>    // cleanup registered above will handle this.<br>    lp = &amp;my_lock ;<br>    pthread_rwlock_wrlock(lp) ;<br>    ..<br>}</pre><h3>Cleanup model</h3><p>Looking at the common patterns of cleanup operations, we can classify them based on the parameters that they take:</p><p>Resource Identifier: pointer/integer</p><ul><li>P (pointer) Most resources are identified by their memory address</li><li>I (integer) Some resources are identified by their integer handle</li></ul><p>In addition, some cleanup operations need extra context:</p><ul><li>X — Extra information is needed for the cleanup operation</li><li>M — Extra information of integer “mode” to distinguish between small set of modes.</li></ul><p>For a total of six possible combinations:</p><ul><li>P — cleanup(void *p)</li><li>I — cleanup(int handle)</li><li>PX — cleanup(void *p, void *cxt)</li><li>IX — cleanup(int handle, void *cxt)</li><li>PM — cleanup(void *p, int mode)</li><li>IM — cleanup(int handle, int mode)</li></ul><p>If a cleanup operation needs more than one extra parameter, it can be modeled by passing all additional parameter inside a single structure that will be passed as the context parameter.</p><p>In practice:</p><ul><li>Most code uses: P</li><li>Some cases need: I — usually for system objects</li><li>Rare cases need: X or M (extra arguments)</li></ul><h4>Using values at block exit</h4><p>Cleanup is performed using the current value of the variable at scope exit.</p><p>This allows resources to be resized, reassigned, or disabled by setting them to their invalid value (NULL, -1, ...).</p><p>In simple cases, we create a resource, and we perform the cleanup by calling the “destructor” function with the same object that was created</p><pre>// Old Code<br>{<br>    char *const x = calloc(n, sizeof(*x)) ;<br>    ...<br>    if ( ... ) { free(x) ; return ; }<br>    ...<br>    free(x) ;<br>}<br>// With DEFER_FREE<br>{<br>    char *const x = calloc(n, sizeof(*x)) ;<br>    DEFER_FREE(x) ;<br>    ...<br>}</pre><p>Since the cleanup is using the resource address at block exit, it works even when the source address is changing. One example is with realloc, when the free function should be invoked with the final value of x:</p><pre>{<br>    char *x = calloc(n, sizeof(*x)) ;<br>    DEFER_FREE(x) ;<br>    ...<br>    x = realloc(x, m*sizeof(*x)) ;<br>    ...<br>}</pre><p>Another case is when the resource is released earlier in the function, and there is no need to perform the cleanup at the end. In those cases possible to reset the resource identifier to protect against double-cleanup.</p><pre>{<br>    char *x = calloc(n, sizeof(*x)) ;<br>    DEFER_FREE(x) ;<br>    ...<br>    free(x) ;<br>    x = NULL ;<br>    ...<br>    return ;<br>}</pre><p>Releasing the memory when it is no longer needed is good practice. In this case, the value of the resource has to be reset, so that the automatic invocation will not attempt to free the block again (which will likely crash the program).</p><blockquote><em>Most resources already have a natural “NA” value (e.g., NULL, -1), which can be used to mark them as already cleaned up.</em></blockquote><h4>Resource Identifier: Pointer vs Integer</h4><p>In many cases, resources are identified by their memory address, and the cleanup function only needs this memory address. Almost anything derived from malloc/calloc (or other allocators). This includes:</p><ul><li>File Object (FILE *) created by fopen, fdopen or popen</li><li>Dynamically created strings created by strdup, strndup</li><li>Network structures created by getaddrinfo and similar</li><li>User defined objects created on the heap.</li></ul><p>The second category of identifiers is handles — resources that are identified by integer handle — in many cases, referencing system resources, outside our process space</p><ul><li>File descriptors (open, creat, socket, ...),</li><li>Process identifiers (fork)</li><li>IPC resources like shared memory (shmget)</li></ul><h4>Extra Context</h4><p>Certain cleanup functions require extra information. For example:</p><ul><li>The shutdown system calls take a parameter (int how).</li><li>The munmap system call takes a length parameter</li></ul><p>We generalize the support for extra parameters by adding support for an extra pointer, which can be used to pass additional required parameters.</p><h4>Naming rules</h4><p>To support all variations, we use consistent naming rules:</p><pre>DEFER_CALL_(P|I)(|X|M)</pre><p>For example:</p><ul><li>DEFER_CALL_P(cleanup, var) — Call cleanup(void *).</li><li>DEFER_CALL_I(cleanup, fd) — Call cleanup(int).</li><li>DEFER_CALL_IM(cleanup, sock, mode) — Call cleanup(int, int), note that mode is set at registration time.</li></ul><h3>Defining Cleanup function for user objects.</h3><p>The DEFER_CALL_* macros can be used to define cleanup helper for objects that have create and destroy functions.</p><pre>struct foo { char *name, ... } ;<br><br>struct foo *fooCreate(char *name, ...) <br>{<br>    struct foo *v = calloc(1, sizeof(*v)) ;<br>    v-&gt;name = strdup(name) ;<br>    ...<br>    return v ;<br>}<br>void fooDestroy(struct foo *p)<br>{<br>    free(p-&gt;name) ;<br>    ...<br>    free(p) ;<br>}<br>void doSomething(void)<br>{<br>    struct foo *foo1 = fooCreate(&quot;name1&quot;) ;<br>    DEFER_DESTROY(fooDestroy, foo1) ;<br>    ...<br>    // foo1 will be automatically destroyed with fooDestroy at exit<br>}</pre><p>If additional parameters are needed any of the other helper macros can be used to specify cleanup functions that takes arbitrary parameters (with DEFER_DESTROY_X), or integer modifier (DEFER_DESTROY_M). The extra parameter (context or mode) is captured at the time of the cleanup registration.</p><p>For example, the fooDestroy may take an integer indicator for logging. Note that the modifier is captured at the time of registration.</p><pre>void fooDestroy(struct foo *p, int verbose)<br>{<br>    if ( verbose ) {<br>        printf(&quot;Destroying: %s\n&quot;, p-&gt;name) ;        <br>    }<br>    free(p-&gt;name) ;<br>    ...<br>    free(p) ;<br>}<br><br>void doSomething(void)<br>{<br>    struct foo *foo1 = fooCreate(&quot;name1&quot;) ;<br>    DEFER_DESTROY_M(fooDestroy, foo1, 1) ;<br>    ...<br>    // foo1 will be automatically destroyed with fooDestroy at exit<br>}</pre><p>If the cleanup function needs additional parameters, possible to model it with a structure that passes all the information in one structure, possible using a compound literal.</p><pre>struct fooArgs { int verbose ; int timeout ; } ;<br><br>void fooDestroy(struct foo *p, struct fooArgs *args)<br>{<br>    if ( args-&gt;verbose ) {<br>        printf(&quot;Destroying: %s\n&quot;, p-&gt;name) ;        <br>    }<br>    free(p-&gt;name) ;<br>    ...<br>    free(p) ;<br>}<br>void doSomething(void)<br>{<br>    struct foo *foo1 = fooCreate(&quot;name1&quot;) ;<br>    DEFER_DESTROY_X(fooDestroy, foo1,<br>        &amp;(struct fooArgs) { .verbose=1, .timeout = 5 }) ;<br>    ...<br>    // foo1 will be automatically destroyed with fooDestroy at exit<br>}</pre><h3>Summary</h3><p>The goal of this project is not to replace a future standard defer, but to provide a small and consistent cleanup layer that works in current (and past) GCC/Clang compilers and can evolve with the language over time.</p><p>In practice, a handful of cleanup macros already eliminate a large amount of repetitive cleanup code in typical C projects, while keeping the behavior explicit and predictable.</p><h3>Disclaimer</h3><p>The cleanup attribute is available with GCC (starting with 3.4.x) and Clang (3.x era and later) as a C compiler extension. Many other compilers support this extension - but I did not test them.</p><p>The examples in this article, including linked code snippets, are simplified and reconstructed for illustration purposes. They are not taken from any production system, and do not reflect the design or implementation of any specific codebase.</p><p>This is a personal approach based on general experience working with C codebases. It does not represent any official guideline or the opinion of my employer.</p><p>As with any low-level technique, evaluate carefully before adopting it in production.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=10bd03c09d64" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/stop-waiting-for-defer-a-practical-cleanup-layer-for-c-10bd03c09d64">Stop Waiting for defer: A Practical Cleanup Layer for C</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automatic Enum Handling in C — Parsing, Validating and Iterating.]]></title>
            <link>https://medium.com/@yair.lenga/automatic-enum-handling-in-c-parsing-validating-and-iterating-76de362f9532?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/76de362f9532</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[c-programming]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Wed, 29 Apr 2026 07:20:22 GMT</pubDate>
            <atom:updated>2026-04-29T07:43:11.330Z</atom:updated>
            <content:encoded><![CDATA[<h3>Automatic Enum Handling in C — Parsing, Validating and Iterating.</h3><h3>Using debug (DWARF) information to eliminate lookup tables and keep enum logic in sync at build time</h3><h3>Quick recap — enum Stringification</h3><p>In previous article <a href="https://medium.com/@yair.lenga/automatic-enum-stringification-in-c-via-build-time-code-generation-659b67133125">Automatic Enum Stringification in C via Build-Time Code Generation</a>, I described a process to extract information about enum types (the list of enum identifiers and values) from debug (DWARF) information. This allows us to display symbolic enum labels instead of numeric values in logs, debug output, and more.</p><pre>enum color { C_NONE, C_RED, C_YELLOW, C_GREEN } ;<br><br>// Request enum descriptor for e_color<br>ENUM_DESCRIBE(e_color, enum color)<br>void foo(enum color c) {<br>    printf(&quot;Color=%s(%d)\n&quot;, ENUM_LABEL_OF(e_color, c), c) ; <br>}</pre><p>The process is fully automated, relies on common tools that are already used in build pipelines, and has (practically) zero runtime cost.</p><h3>Parsing Strings into Enums</h3><p>The next logical step is the reverse conversion — from symbolic name to value. This is useful when reading external input keyed by enum values: command-line arguments, configuration files, or user input.</p><p>Languages that support reflection (Java, Python, C#, ...) will usually provide lookup functions (EnumClass.valueOf(String), Enum.Parse(...)), but in C, there is no built-in, standard capability.</p><p>This article discusses a lightweight solution to parse strings into enum values, so that you can write parsing functions:</p><pre>ENUM_DESCRIBE(e_color, enum color)<br><br>bool parse_color(const char *label, enum color *var)<br>{<br>    return ENUM_PARSE_LABEL(e_color, label, var) ;<br>}</pre><p>No hard-coded lookup tables. Always kept in sync with the enum definition at build time, using tools you already have.</p><blockquote><em>In practice, this means enum labels can be part of the input interface, not just internal constants.</em></blockquote><h4>Download &amp; Quick Start</h4><p>Download the latest minimal package (~20KB):</p><p><a href="https://github.com/yairlenga/c-enum-reflect/releases/latest">https://github.com/yairlenga/c-enum-reflect/releases/latest</a></p><p>See the <a href="https://github.com/yairlenga/c-enum-reflect/releases/">Releases</a> page for other versions and packages.</p><h4>Example: Reading input</h4><p>Let’s assume we have in memory information about each color (e.g., RGB values) and we want to allow the user to choose the RGB by color name, which is an enum. Currently, we will write a lookup table, or inline a list of strcmp to map the input label into the correct enum index.</p><pre>enum color { RED, YELLOW, GREEN } ;<br><br>bool parse_color(const char *label, enum color *var)<br>{<br>    if ( strcmp(label, &quot;RED&quot;) == 0) *var = RED ;<br>    else if ( strcmp(label, &quot;YELLOW&quot;) == 0) *var = YELLOW ;<br>    else if ( strcmp(label, &quot;GREEN&quot;) == 0) *var = GREEN ;<br>    else return false ;<br>    return true ;<br>}<br>int rgb[] = {<br>    [RED] = 0xff0000,<br>    [YELLOW] = 0xffff00,<br>    [GREEN] = 0x00ff00,<br>} ;<br>int show_rgb(const char *label)<br>{<br>    enum color c ;<br>    if ( parse_color(label, &amp;c)) {<br>        printf(&quot;RGB(%s)=%06x\n&quot;, label, rgb[c]) ;<br>    } else {<br>        printf(&quot;Unknown Color: &#39;%s&#39;\n&quot;, label) ;<br>    }<br>}</pre><p>In most cases, we will use a lookup table to make the code easier to maintain.</p><pre>enum color { RED, YELLOW, GREEN } ;<br><br>bool parse_color(const char *label, enum color *var)<br>{<br>    static struct { enum color c; const char *label ; } color_lookup[] = {<br>        { RED, &quot;RED&quot; },<br>        { YELLOW, &quot;YELLOW&quot; },<br>        { GREEN, &quot;GREEN&quot; },<br>    } ;<br>    for (int i=0 ; i&lt;sizeof(color_lookup)/sizeof(color_lookup[0]) ; i++) {<br>        if ( strcmp(label, color_lookup[i].label) == 0 ) {<br>                *var = color_lookup[i].c ;<br>                return true ;<br>        }<br>    }<br>    return false ;<br>}</pre><p>Either way, we will have the same problem that we had with enum stringification:</p><ul><li>Requires repetitive work.</li><li>Easy to miss updates, or introduce incorrect fixes.</li><li>Hard to maintain if enum is defined in external packages.</li></ul><p>The good news is that we can build directly on the existing enum metadata to support parsing.</p><h4>API for Parsing Enum</h4><p>The enum_desc module provides a basic API to translate a string into the enum value. It builds on the ENUM_DESCRIBE macro which we use to create the enum descriptions.</p><pre>// Define a tag that reference the enum<br>ENUM_DESCRIBE(tag, enum_type)<br><br>// Function-like macro.<br>// If the label matches one of the enum labels:<br>//    store the matching enum value into the variable that p_enum points to.<br>//    return true<br>// On error:<br>//    does NOT modify the variable pointed to by p_enum<br>//    return false<br>bool ENUM_PARSE_LABEL(enum_tag, const char *label, enum_type *p_enum) ;</pre><p>Our parsing function is now simple, short:</p><pre>ENUM_DESCRIBE(e_color, enum color)<br><br>bool parse_color(const char *label, enum color *var)<br>{<br>    return ENUM_PARSE_LABEL(e_color, label, var) ;<br>}</pre><p>No strcmp, no lookup tables to create, and no updates needed when enum values change.</p><h4>Example: parsing values from a config file</h4><p>Using the new API, we can now read a configuration file that describes RGB values for colors, leveraging the symbolic names.</p><pre>// colors.txt<br>FOREGROUND = 0xff0000<br>BACKGROUND = 0xffff00<br>HIGHLIGHT = 0x00ff00<br>BOLD  = 0xff0000</pre><p>And the code is relatively simple.</p><pre>enum color { FOREGROUND, BACKGROUND, HIGHLIGHT, BOLD, LAST } ;<br><br>int rgb[LAST] ;<br>void read_rgb(FILE *fp)<br>{<br>    char line[256] ;<br>    while ( fgets(line, sizeof(line), fp)) {<br>        enum color c ;<br>        char color_name[30] ;<br>        int rgb_value ;<br>        if ( sscanf(line, &quot;%29s = %x&quot;, color_name, &amp;rgb_value) == 2 &amp;&amp;<br>            parse_color(color_name, &amp;c) ) {<br>                rgb[c] = rgb_value ;<br>        }<br>    }<br>}</pre><p>Later, we will discuss other options to make the code more flexible — e.g., resizing to the maximum values of the enum (or the actual number of entries).</p><h3>Validation and Error Reporting</h3><p>When the ENUM_PARSE_LABEL() call fail to match the label, it will return false, and will <strong>not</strong> modify the enum variable. This can be used to introduce defaults, and error logging as needed. For example:</p><pre>enum color { FOREGROUND, BACKGROUND, HIGHLIGHT, BOLD, LAST } ;<br><br>int rgb[LAST] = {<br>    [FOREGROUND] = 0x000000,  // Black<br>    [BACKGROUND] = 0xffffff,  // White<br>    [HIGHLIGHT] = 0xffff00,   // Yellow<br>    [BOLD] = 0xff0000,        // Red<br>} ;<br>ENUM_DESCRIBE(e_color, enum color)<br>bool parse_color(const char *label, enum color *var)<br>{<br>    return ENUM_PARSE_LABEL(e_color, label, var) ;<br>}<br>void read_rgb(FILE *fp)<br>{<br>    char line[256] ;<br>    while ( fgets(line, sizeof(line), fp)) {<br>        char color_name[30] ;<br>        int rgb_value ;<br>        if ( sscanf(line, &quot;%29s = %x&quot;, color_name, &amp;rgb_value) != 2 ) {<br>            fprintf(stderr, &quot;Bad Config line: &#39;%s&#39;\n&quot;, line) ;<br>            continue ;<br>        }<br>        enum color c ;<br>        if ( !parse_color(color_name, &amp;c) ) {<br>            fprintf(stderr, &quot;Unknown color: &#39;%s&#39;\n&quot;, color_name) ;<br>            continue ;<br>        }<br>        rgb[c] = rgb_value ;<br>    }<br>}</pre><p>Once enum metadata is available, parsing is not the only operation we can support. We can also iterate over all enum values, enabling generic processing and introspection.</p><h3>Iterating Over Enum Values</h3><h4>Enumeration API</h4><p>Given that the enum metadata is stored in a simple to iterate format, it’s possible to iterate over all the values of a single enum. The API provides a few functions to query the enum list</p><ul><li>The enum_desc_item_count(), returns the number of enumerators.</li><li>The enum_desc_label_at() returns the label (const char *) of the enumerator, based on position. Return NULL on bad position.</li><li>The enum_desc_value_at() returns the integer value (int) of the enumerator, based on position. Return 0 on bad position.</li></ul><p>Both function take an enum_desc_t object. The function-like macro ENUM_DESC(tag) can be used to get the descriptor. The following code can list all the enumerators of an enum type:</p><pre>enum color { NONE, BACKGROUND, FOREGROUND, HIGHLIGHT, BOLD, LAST } ;<br><br>ENUM_DESCRIBE(e_color, enum color)<br>    // Showing all colors, using count<br>void show_color_enum()<br>{<br>    enum_desc_t ed = ENUM_DESC(e_color) ;<br>    for (int i=0, count=enum_desc_item_count(ed) ; i&lt;count ; i++) {<br>        printf(&quot;Color #%d: %s = %d\n&quot;, i, enum_desc_label_at(ed, i), enum_desc_value_at(ed, i)) ;<br>    }<br>}</pre><h4>Dumping all enum values</h4><p>The reflection API can work on any enum - the above code is generic. The enum_desc_print dump all the data about an enum in human readable format:</p><pre>// ISO country codes.<br>enum country3 {<br>    ...<br>    ISO3_BGR = 100, ISO3_MMR = 104, ISO3_BDI = 108,<br>    ISO3_BLR = 112, ISO3_KHM = 116, ISO3_CMR = 120,<br>    ISO3_CAN = 124, ISO3_CPV = 132, ISO3_CYM = 136,<br>    ...<br>}</pre><p>Will print:</p><pre>Enum &#39;country3&#39; 191 items, dynamic=TRUE, custom=FALSE, offset_sz=2, value_sz=4<br>...<br>#12: 44 (ISO3_BHS) meta=-<br>#13: 48 (ISO3_BHR) meta=-<br>#14: 50 (ISO3_BGD) meta=-<br>#15: 51 (ISO3_ARM) meta=-<br>#16: 52 (ISO3_BRB) meta=-<br>#17: 56 (ISO3_BEL) meta=-<br>#18: 60 (ISO3_BMU) meta=-<br>...<br>Range: [ 4 - 894 ] , unused=700</pre><h4>Using iteration for generic tools</h4><p>The iteration API can also be used to provide custom behavior. For example, to lookup for enumerator using case insensitive match. This will allow referencing the enum using uppercase, lowercase of mixed-case.</p><pre>bool enum_parse_case_cmp(enum_desc_t ed, const char *label, int *var)<br>{<br>    int count = enum_desc_item_count(ed) ;<br>    for (int i=0 ; i&lt;count ; i++) {<br>        if ( strcasecmp(label, enum_desc_label_at(ed, i)) == 0) {<br>            *var = enum_desc_value_at(ed, i) ;<br>            return true ;<br>        }<br>    }<br>    return false ;<br>}</pre><h3>How it works</h3><p>This is a summary — see more details in <a href="https://medium.com/@yair.lenga/automatic-enum-stringification-in-c-via-build-time-code-generation-659b67133125#66cc">previous article </a>— including Makefile for CI, pre-requisitistes, …</p><p>The process happens entirely at build time.</p><ol><li>Compile the source file with debug information.</li><li>Scan the object file and extract the enum definition (via DWARF)</li><li>Generate a C source file containing enum descriptors.</li><li>Compile and link the generated code into the final binary.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MMiulCU65aseC2su5kSnAg.png" /></figure><h4>Example:</h4><pre>gcc -c -g test2.c<br>enum_dwarf_query --format=c test2.o &gt; gen_test2.c<br>gcc -c -g gen_test2.c<br>gcc -g -o prog.exe test2.o enum_desc.o gen_test2.o</pre><p>The final binary contains only plain C data structures, and a small runtime support module (from enum_desc.c). There is no runtime dependency on DWARF tools or libraries, or on a debugger.</p><h3>Summary</h3><ul><li>Enum metadata is generated automatically from debug (DWARF) information.</li><li>The same data supports parsing, validation, and iteration.</li><li>No manual lookup tables or duplicated definitions.</li><li>Always in sync with the compiled enum — including external libraries.</li><li>A small step toward practical reflection in C.</li></ul><h4>Notes</h4><p>In addition to the generic API, typed helper functions such as enum_desc_label_of_color() and enum_desc_parse_color() can be generated for convenience. These wrappers improve type safety and readability, but are not required — the generic API is sufficient in most cases.</p><h4>Disclaimer</h4><p>The examples and benchmarks in this article, including linked code snippets, are simplified and reconstructed for illustration purposes. They are not taken from any production system, and do not reflect the design or implementation of any specific codebase.</p><p>This is a personal approach based on general experience working with C codebases. It does not represent any official guideline or the opinion of my employer.</p><p>As with any low-level technique, evaluate carefully before adopting it in production.</p><h4>Usage and License</h4><p>The supporting files (enum_desc.c, enum_desc.h, enum_dwarf_query.py) are provided under the MIT license and are intended to be copied and used as-is in your own projects.</p><p>You can simply copy and/or modify them into your project and integrate the extractor into your build process — no special packaging or setup is required</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=76de362f9532" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automatic Enum Stringification in C via Build-Time Code Generation]]></title>
            <link>https://medium.com/@yair.lenga/automatic-enum-stringification-in-c-via-build-time-code-generation-659b67133125?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/659b67133125</guid>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[coding]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Thu, 23 Apr 2026 15:29:05 GMT</pubDate>
            <atom:updated>2026-04-29T17:33:03.122Z</atom:updated>
            <content:encoded><![CDATA[<h4>Leveraging compiler debug metadata (DWARF) to generate enum mappings with zero runtime overhead</h4><p>The followup article: <a href="https://medium.com/@yair.lenga/automatic-enum-handling-in-c-parsing-validating-and-iterating-76de362f9532">Automatic Enum Handling in C — Parsing, Validating and Iterating</a> Covers additional topics related to using enum metadata.</p><p>If you maintain C code, you’ve probably written enum-to-string conversion functions by hand. They work — until someone adds a new enum value and forgets to update them.</p><p>When the enum values are assigned sequential values, it is possible to perform fast lookup with arrays, using designated initializers:</p><pre>enum ConnectionState {<br>    STATE_NONE,<br>    STATE_DISCONNECTED,<br>    STATE_CONNECTING,<br>    STATE_CONNECTED,<br>    STATE_ERROR,<br>    STATE_LAST<br>};<br><br>const char *connectionStateStr(enum ConnectionState state) {<br>    static const char *labels[] = {<br>        [STATE_NONE] = &quot;STATE_NONE&quot;,<br>        [STATE_DISCONNECTED] = &quot;STATE_DISCONNECTED&quot;,<br>        [STATE_CONNECTING] = &quot;STATE_CONNECTING&quot;,<br>        [STATE_CONNECTED] = &quot;STATE_CONNECTED&quot;,<br>        [STATE_ERROR] = &quot;STATE_ERROR&quot;,<br>    } ;<br>    return state &gt;= 0 &amp;&amp; state &lt; STATE_LAST ? labels[state] : NULL ;<br>}</pre><p>In the other case (e.g. the enum values span a sparse range), you might have implemented it with a switch statement (or some form of lightweight hash table)</p><pre>enum errorCode {<br>    E_NOT_FOUND = -1,<br>    E_PERMISSION = -2,<br>    E_OUT_OF_MEMORY = -3,<br>    ...<br>} ;<br><br>const char *errorCodeStr(enum errorCode code) {<br>    switch (code) {<br>        case E_NOT_FOUND: return &quot;E_NOT_FOUND&quot; ;<br>        case E_PERMISSION: return &quot;E_PERMISSION&quot; ;<br>        case E_OUT_OF_MEMORY: return &quot;E_OUT_OF_MEMORY&quot; ;<br>        ...<br>    } ;<br>    return NULL ;<br>}</pre><p>Those lookups are commonly used to create log records, parse configuration options, and print debug output. This implementation has a few limitations:</p><ul><li>Requires repetitive work.</li><li>Easy to miss updates, or introduce incorrect fixes.</li><li>Hard to maintain if enum is defined in external packages.</li></ul><p>Languages that support reflection (Java, Python, C#, ...) will usually provide a stringification function, but in C, there is no built-in, standard capability.</p><p>This article discusses a lightweight solution to create stringification functions, so that you can write:</p><pre>printf(&quot;Connection State=%s\n&quot;, ENUM_LABEL_OF(ConnectionState, state)) ;</pre><p>No hard-coded lookup tables. Always kept in sync with the enum definition at build time, using tools you already have.</p><h3>Quick Start</h3><p>Download the latest minimal package (~20KB):</p><p><a href="https://github.com/yairlenga/c-enum-reflect/releases/latest">https://github.com/yairlenga/c-enum-reflect/releases/latest</a></p><p>See the <a href="https://github.com/yairlenga/c-enum-reflect/releases/">Releases</a> page for other versions and packages.</p><h3>Solution — automatic stringification of enum values</h3><p>If C had reflection, we would have the option to write something like:</p><pre>enum color { C_NONE, C_RED=2, C_YELLOW=6, C_GREEN } ;<br>void foo(enum color c)<br>{<br>    printf(&quot;Color=%s(%d)\n&quot;, color.to_string(c), c) ;<br>}</pre><p>The <em>bad</em> news is that C does not provide this capability directly. The <em>good</em> news is that the compiler already has all the information needed to implement it. When code is compiled with debug (-g), the full definition of each (referenced) enum is captured in the object file. <strong>We can reuse it !</strong></p><p>We can verify this using a debugger such as gdb. gdb will show the symbolic value of each enum variable (with print), and the full enum description with ptype.</p><pre>(gdb) print c<br>$1 = C_RED<br>(gdb) ptype c<br>type = enum color {C_NONE, C_RED = 2, C_YELLOW = 6, C_GREEN}</pre><p>See appendix <a href="#background-full-example-for-enum-metadata-with-gdb">Background: Full example for </a><a href="#background-full-example-for-enum-metadata-with-gdb">enum metadata with </a><a href="#background-full-example-for-enum-metadata-with-gdb">gdb</a> at the end.</p><p>Instead of using this metadata only for debugging, we can extract it at build time and generate lookup tables automatically. This effectively provides reflection for enums in C - without any runtime cost.</p><h3>Minimal example — using generated enum descriptions</h3><pre>// test2.c<br>#include &lt;stdio.h&gt;<br>#include &quot;enum_desc.h&quot;<br><br>enum color { C_NONE, C_RED, C_YELLOW, C_GREEN } ;<br>// Request enum descriptor e_color that will describe enum color<br>ENUM_DESCRIBE(e_color, enum color)<br>void foo(enum color c) {<br>    printf(&quot;Color=%d\n&quot;, c) ;<br>    // print stringified label for c<br>    printf(&quot;Color=%s\n&quot;, ENUM_LABEL_OF(e_color, c)) ; <br>}<br>int main(void) {<br>    foo(C_RED) ;<br>    return 0 ;<br>}</pre><h3>How it works</h3><p>The process happens entirely at build time.</p><ol><li>Compile the source file with debug information.</li><li>Scan the object file and extract the enum definition (via DWARF)</li><li>Generate a C source file containing enum descriptors.</li><li>Compile and link the generated code into the final binary.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qDTTydg4jg7G1ojqecXU6Q.png" /></figure><h4>Example:</h4><pre>gcc -c -g test2.c<br>enum_dwarf_query --format=c test2.o &gt; gen_test2.c<br>gcc -c -g gen_test2.c<br>gcc -g -o prog.exe test2.o enum_desc.o gen_test2.o</pre><p>The final binary contains only plain C data structures, and a small runtime support module (from enum_desc.c). There is no runtime dependency on DWARF tools or libraries, or on a debugger.</p><h3>Notes on ENUM_DESCRIBE, ENUM_LABEL_OF</h3><p>The ENUM_DESCRIBE macro marks enum types that we want to generate metadata for.</p><ul><li>The first argument (e_color) is a unique identifier for the descriptor.</li><li>The second argument must resolve to a valid enum type, in the current translation unit.</li><li>The enum can be defined in the same file or in an included header file.</li><li>Important: the descriptors are generated as global symbols. Using the same identifier (for the same or for different enums) will result in link-time error.</li></ul><p>The ENUM_LABEL_OF macro is expanded to a call to retrieve the generated metadata.</p><ul><li>The first argument is the unique identifier.</li><li>The second argument is an enum value to be described.</li><li>Returns NULL, if enum value does not have a label.</li></ul><p>You can view the definition of those macros in GitHub Gist: enum_desc.h. You will see that the implementation is defining multiple identifiers - all follow the pattern enum_desc_*. Some identifiers have static scope, some are global objects (functions, variables). If you inspect the objects/binaries, you will see those symbols.</p><h3>Integration into CI pipeline</h3><p>In practice — most projects use a build system (Make or CMake). So the generated files are rebuilt automatically when the source object file is rebuilt. This will ensure that the descriptor is up-to-date, even for enums that are defined in header files (current project, dependent objects, or system header files).</p><p>Adding the following to your Makefile will automate the build:</p><pre># ENUMDESC_DIR - Source location where enum_desc source files are (.c, .h and python parser)<br># ENUMDESC_SRCS - list of source files that have call ENUM_DESCRIBE<br># OBJDIR - directory where generated files (objects and sources) will be stored.<br># PROG - path to binary, which should link generated enum_* objects.<br><br>CFLAGS += -I $(ENUMDESC_DIR)<br>ENUMDESC_SRCS = file1.c file2.c ...<br><br>$(OBJDIR)/gen_%.o: $(OBJDIR)/%.o<br>    $(ENUMDESC_DIR)/enum_dwarf_query --format=c $&lt; &gt; $(OBJDIR)/gen_$*.c<br>    $(COMPILE.c) -o $@ $(OBJDIR)/gen_$*.c<br><br>$(OBJDIR)/enum_desc.o: $(ENUMDESC_DIR)/enum_desc.c<br>    $(COMPILE.c) -o $@ $^<br><br>ENUMDESC_OBJS += $(OBJDIR)/enum_desc.o $(ENUMDESC_SRCS:%.c=$(OBJDIR)/gen_%.o)<br>$(PROG): ... $(ENUMDESC_OBJS)<br>    $(LINK.c) -o $@ ... $(ENUMDESC_OBJS)</pre><p>Note that this pipeline requires (reasonable modern) python3 runtime, including the pyelftools:</p><pre>sudo apt install python3-pyelftools<br>sudo python3 -m pip install pyelftools</pre><p>There is no runtime dependency on DWARF, debug information, or external tools. The binary can be fully stripped — as if nothing unusual ever happened.</p><h3>Why this approach</h3><p>Before settling on this approach, I’ve experimented with a few other alternatives. The main challenges were</p><ol><li>Keeping definitions in sync as enum values evolve.</li><li>Minimizing effort when adding new enums.</li><li>Maintaining single source of truth.</li><li>Avoiding unnecessary complexity.</li></ol><p>The options that I’ve considered were</p><ul><li>X-Macros: Require rewriting enums into a custom format, which many codebases cannot or will not adopt.</li><li>DSL-style (IDL files, proto): Do not work for enums defined outside your control (external libraries, system headers).</li><li>Manual Lookup Tables: sooner or later, the mapping falls out of sync with the enums.</li><li>Parsing Source files (AST tools, Clang toolkit, regex): Parsing C correctly is hard: anything less than a full parser is fragile, and may fail in the future.</li><li>Compiler Plugins: Powerful, but tie the solution to single compiler/toolchain, require significant effort to develop and maintain.</li></ul><p>While using the DWARF metadata is not perfect for all situations, it avoids the challenges of the other alternatives:</p><ul><li>It stays in sync with the source of truth — the way the compiler understands the enum.</li><li>It requires no changes to existing source code and introduces no runtime dependency.</li><li>It is built on tools already common in C development (gcc, clang, gdb), established and widely-used standard format (DWARF), and simple open source components (python, pyelftools)</li></ul><p>As an extra bonus — the generated “C” code can be easily inspected/reviewed — no magic, no complex runtime, no black boxes.</p><h3>Summary</h3><p>Enum stringification in C is a common problem, typically solved with manual lookup tables, or custom definitions — both have a maintenance cost and tend to drift out of sync over time. This approach addresses this problem using a different path: reusing the debug metadata already produced by the compiler to generate enum descriptors at build time.</p><p>The result is straightforward:</p><ul><li>No changes to existing enum definitions in the source code.</li><li>No duplicate definitions.</li><li>No run-time dependency on external tools or libraries.</li><li>Always in sync with the enum as compiled.</li></ul><p>In practice, the compiler does the heavy lifting — we just reuse it.</p><p>This is just the first step — once the metadata is available, it can also be used for parsing configuration files, validation and more. A follow-up article will explore those use cases.</p><h3>Appendices</h3><h4>Background: Full example for enum metadata with gdb</h4><p>Here is a small program that shows enum metadata:</p><pre>// color.c<br>#include &lt;stdio.h&gt;<br><br>enum color { C_NONE, C_RED=2, C_YELLOW=6, C_GREEN } ;<br>void foo(enum color c) {<br>    printf(&quot;Color=%d\n&quot;, (int) c) ;                          // print as integer<br>}<br>int main(void) {<br>    foo(C_RED) ;<br>    return 0 ;<br>}</pre><p>We can compile with debug information (gcc -g), and inspect the enum with gdb:</p><pre>$ gcc -g color.c<br>$ gdb a.out<br>Reading symbols from a.out...<br>(gdb) b foo<br>Breakpoint 1 at 0x1158: file color.c, line 6.<br>(gdb) run<br>Starting program: /home/user/github/articles/a.out <br>Breakpoint 1, foo (c=C_RED) at color.c:6<br>6           printf(&quot;Color=%d\n&quot;, (int) c) ;<br>(gdb) print c<br>$1 = C_RED<br>(gdb) ptype c<br>type = enum color {C_NONE, C_RED = 2, C_YELLOW = 6, C_GREEN}</pre><p>The information comes from the DWARF debug metadata that is embedded in the object file, and is available to the debugger (usually, it’s embedded in the executable).</p><h3>Disclaimer</h3><p>The examples and benchmarks in this article, including linked code snippets, are simplified and reconstructed for illustration purposes. They are not taken from any production system, and do not reflect the design or implementation of any specific codebase.</p><p>This is a personal approach based on general experience working with C codebases. It does not represent any official guideline or the opinion of my employer.</p><p>As with any low-level technique, evaluate carefully before adopting it in production.</p><h3>Usage and License</h3><p>The supporting files (enum_desc.h, enum_desc.c, enum_dwarf_query.py ) are provided under the MIT license and are intended to be copied and used as-is in your own projects.</p><p>You can simply copy and/or modify them into your project and integrate the extractor into your build process — no special packaging or setup is required</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=659b67133125" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Optimizing Chained strcmp Calls for Speed and Clarity — Without Refactoring]]></title>
            <link>https://blog.stackademic.com/optimizing-chained-strcmp-calls-for-speed-and-clarity-without-refactoring-b57035b78f18?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/b57035b78f18</guid>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[string-manipulation]]></category>
            <category><![CDATA[low-level-programming]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Mon, 13 Apr 2026 19:35:11 GMT</pubDate>
            <atom:updated>2026-04-21T10:29:24.858Z</atom:updated>
            <content:encoded><![CDATA[<h3>Optimizing Chained strcmp Calls for Speed and Clarity</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/717/1*hBN85iXGkAjRconueGJSXg.png" /></figure><h3>From memcmp and bloom filters to 4CC encoding for small fixed-length string comparisons</h3><p>While working on a financial modeling system, we started noticing a gradual degradation in performance. In the beginning — nothing dramatic, just a steady increase in runtime, as the code evolved over years of use.</p><p>After some profiling, the issue was traced to a core module where a critical part of business logic was encoded. The structure of the code was not an accident — it started small, and grew in complexity over the years — conditions, edge cases, and date-based complexities.</p><h3>The Code We Started With</h3><p>At the center were functions that took action based on currency codes (ISO 3-letter codes), and an as-of date. One specific function was used to load the correct modeling parameters into a configuration structure.</p><pre>bool model_ccy_lookup(const char *s, int asof, struct model_param *param)<br>{<br>    // Major Currencies<br>    if ( strcmp(s, &quot;USD&quot;) == 0 || strcmp(s, &quot;EUR&quot;) == 0 || ...) {<br>        ...<br>    // Asia-Core<br>    } else if ( strcmp(s, &quot;CNY&quot;) == 0 || strcmp(s, &quot;HKD&quot;) == 0 || ... ) {<br>        ...<br>    } else if ( ... ) {<br>        ...<br>    } else {<br>        ...<br>    }<br>}</pre><p>The above is a simplified version. The actual logic was more involved, but the structure was the same — long chains of strcmp, grouped by common business rules. <a href="https://gist.github.com/yairlenga/0d429e068dcac5adb605a308ca85f1e8">See full code — Github Gist</a>.</p><h3>When It Became a Bottleneck</h3><p>The slowdown wasn’t caused by a single change. It was gradual — over the years. Each added currency introduced a little more work. The chain was organized by “popularity” — major currencies ended up with 2–3 strcmp calls, and that was never changed. But the average cost went up, users of the system for &#39;non-major&#39; currencies noticed a significant and growing performance penalty.</p><p>Profiling quickly revealed the issue. The block was executed frequently, and most of the work was repeated strcmp. In the worst case scenario, none of the conditions match, which meant repeated sequences of failed strcmp.</p><p>Refactoring the logic into lookup structure was not a possibility. The conditions were tied to business rules that had to stay explicit, visible and auditable. Specifically, each block had nested if, based on the asof date parameter. So many sub-conditions looked like the example below - clean business logic, but expensive to execute.</p><pre>if ( strcmp(s, &quot;USD&quot;) == 0 || strcmp(s, &quot;EUR&quot;) == 0 || ...) {<br>        *param = major_ccy_param ;<br>        if ( strcmp(s, &quot;USD&quot;) == 0 ) {<br>            if ( asof &lt; USD_CUTOFF ) {<br>                param-&gt;p1 = ... ;<br>            }<br>        } else if ( strcmp(s, &quot;EUR&quot;) == 0 ) {<br>            if ( asof &lt; EUR_CUTOFF ) {<br>                param-&gt;p2 = ... ;<br>            }<br>        }<br>    }</pre><h3>Making strcmp Less Expensive</h3><h4>Re-coding the conditions</h4><p>The first step was not about performance. It was about making the code easier to change.</p><p>Instead of writing:</p><pre>if (strcmp(s, &quot;USD&quot;) == 0 || strcmp(s, &quot;EUR&quot;) == 0 || ...)</pre><p>we introduced a small helper:</p><pre>#define CCY_EQ(x, y) (strcmp((x), (y)) == 0)</pre><p>This didn’t make things faster, but it gave us a single place to experiment with different implementations.</p><h4>Reducing function calls</h4><p>The first observation was simple: most of these checks fail.</p><p>A typical path would evaluate several CCY_EQ calls — sometimes 3–5, sometimes more than 10 — before finding a match or falling through. In those cases, we were paying the cost of a full strcmp call each time. (Note: the code was optimized for the &quot;USD&quot; case, which requires only 2 calls, both returning true)</p><p>Since most comparisons fail early, and a significant part of the cost is the function call itself, we tried a small change: (tagging it as strcmp).</p><pre>static inline bool CCY_EQ(const char *x, const char *ccy) <br>{<br>    return x[0] == ccy[0] &amp;&amp; strcmp(x+1, ccy+1) == 0 ;<br>}</pre><p>This turns each comparison into:</p><ul><li>a cheap first-character check</li><li>followed by a strcmp only if needed</li></ul><p>In practice, this brought the cost of a failed comparison much closer to a single character test.</p><h4>Faster compare with memcmp</h4><p>The next observation was also easy — all currency codes are short, and have fixed size — 4 characters (including the terminating NUL). There is no need to use strcmp to find the string end. Instead, we compare with fixed number of bytes.</p><pre>static inline bool CCY_EQ(const char *s, const char *ccy)<br>{<br>    return memcmp(s, ccy, 4) == 0;<br>}</pre><p>As a bonus, memcmp with fixed size is typically inlined by the compiler into a fast sequence of load/compare, avoiding function call overhead entirely.</p><p>This alone gave noticeable improvement, on top of the char compare + strcmp approach.</p><h4>Full inlining</h4><p>As an experiment, we pushed the idea of single byte compare, and expanded the comparison into explicit character checks:</p><pre>static inline bool CCY_EQ(const char *s, const char *ccy)<br>{<br>   return s[0] == ccy[0] &amp;&amp; s[1] == ccy[1] &amp;&amp; s[2] == ccy[2] &amp;&amp; s[3] == ccy[3] ;<br>}</pre><p>This had an interesting side effect. Both gcc and clang were able to optimize these expressions quite aggressively — reordering comparisons, and even combining conditions across different branches.</p><p>For example, currencies with a common prefix (like “INR” and “IDR”) would share part of the decision path, reducing redundant checks.</p><p>At this point, we had significantly reduced the cost of each comparison. But the structure of the code was still the same — long chains of conditions — and the total cost still grew with the number of entries.</p><h4>Summary — improving on strcmp</h4><p>The performance gains are impressive — small, local changes to the implementation resulted in up to <strong>5.7X speedup</strong> over strcmp. Leveraging compiler optimization takes those improvements further - up to <strong>8X in the best case scenario</strong>.</p><p>The benchmark approximates a realistic distribution: most calls target a small set of major currencies, and fewer calls to the other currencies. A smaller number of calls (&lt;1%) were made with currencies that were not handled in the lookup logic.</p><p>The test was executed with various compilation flags — both for gcc and for clang: -Og, -O, -O2 and the aggressive -O3.</p><p>The table below summarizes relative performance. The baseline case is strcmp, compiled with -O2, normalized to 1.0. Higher scores are faster.</p><pre>GCC             -Og   -O    -O2    -O3<br>--------------- ----  ----  ----   ----<br>ccy-01-strcmp   0.95  1.00  1.00   0.93<br>ccy-02-strcmp1  1.11  1.97  2.34   6.27<br>ccy-03-memcmp   0.81  0.87  5.71   5.47<br>ccy-04-charcmp  1.09  2.57  2.62   8.01<br>CLANG<br>ccy-01-strcmp   1.00  1.00  0.99  0.95<br>ccy-02-strcmp1  3.22  3.22  4.43  4.40<br>ccy-03-memcmp   5.58  5.34  5.62  5.19<br>ccy-04-charcmp  4.03  4.68  8.18  7.76</pre><h4>Key takeaway:</h4><blockquote><em>Even without changing the structure of the code, the cost of each failed comparison can be reduced dramatically.</em></blockquote><h3>Cleaning It Up — and Breaking Performance</h3><p>We managed to address the performance problem. Now, it’s time to address the code quality problem — make it easier to maintain, audit, and read. The preferred solution was to replace the chained-if with single calls: CCY_IN: (<a href="https://gist.github.com/yairlenga/28c84632f4bb9c3270bc886b5f2893e6">See full code as GitHub Gist.</a>).</p><pre>// BEFORE - chained IF<br>if (CCY_EQ(s, &quot;USD&quot;) || CCY_EQ(s, &quot;EUR&quot;) || CCY_EQ(s, &quot;JPY&quot;) ||<br>        CCY_EQ(s, &quot;GBP&quot;) || CCY_EQ(s, &quot;CHF&quot;) || CCY_EQ(s, &quot;CAD&quot;) ||<br>        CCY_EQ(s, &quot;AUD&quot;)) {<br>            ...<br>        }<br><br>// AFTER- Similar to SQL &quot;IN&quot; clause.<br>if (CCY_IN(s, &quot;USD&quot;, &quot;EUR&quot;, &quot;JPY&quot;, &quot;GBP&quot;, &quot;CHF&quot;, &quot;CAD&quot;, &quot;AUD&quot;)) {<br>    ...<br>}</pre><p>The initial implementation was simple:</p><pre>static inline bool ccy_in(const char *s, const char **ccy_list)<br>{<br>    while (*ccy_list) {      <br>        if (CCY_EQ(s, *ccy_list))<br>            return true;<br>        ccy_list++;<br>    }<br>    return false;<br>}<br><br>#define CCY_IN(ccy, ...) ({ \<br>    static const char *ccy_list[] = { __VA_ARGS__, NULL } ; \<br>    ccy_in(ccy, ccy_list) ; \<br>    })</pre><p>But the results were poor — worse than the equivalent chained-if implementation.</p><h3>Exploring Alternatives</h3><p>We tried different directions — and they all hit the same wall.</p><ul><li>Applying the strcmp speed-up &#39;tricks&#39; (the strcmp1 and memcmp variants), improved the results but still significantly below the chained-if approach. Getting the best outcome was challenging - required fine-tuning the various compiler options - which is not ideal for maintainability.</li><li>We tried to “flatten” the data structure — moved from const char ** to const char [][4] - expecting the reduced indirection to improve performance. Result: No impact (test ccy-24-strin-4).</li><li>As an experiment, the code was modified to use a bloom filter to reduce the number of comparisons — making the code more complex. The net effect of higher “fixed cost” associated with every call was lower performance.</li></ul><p>The table below summarizes relative performance of various CCY_IN implementations. The baseline case is strcmp, compiled with -O2, normalized to 1.0. Higher scores are faster. The best score (4.8X for the &#39;memcmp) is almost 2X slower vs the best score of the chained-if approach (8X).</p><pre>GCC                   -Og   -O    -O2    -O3<br>--------------------- ----  ----  ----   ----<br>strcmp (BASELINE)     0.95  1.00  1.00   0.93<br>ccy-21-strin          0.75  0.91  0.91   0.88<br>ccy-22-strin-4        0.72  0.90  0.89   0.88<br>ccy-23-strin-cmp1     0.87  2.03  2.08   2.66<br>ccy-24-strin-memcmp   0.63  0.83  3.41   3.38<br>ccy-25-strin-filter   1.30  2.83  3.75   3.60<br>ccy-26-strin-filter4  1.29  2.70  3.74   3.58<br><br>CLANG<br>ccy-21-strin          0.81  0.87  1.01   0.87<br>ccy-22-strin-4        0.85  0.81  1.01   0.98<br>ccy-23-strin-cmp1     1.99  1.67  3.00   3.00<br>ccy-24-strin-memcmp   3.27  2.75  4.80   5.05<br>ccy-25-strin-filter   2.96  2.40  3.69   3.44<br>ccy-26-strin-filter4  2.81  2.85  3.35   3.47</pre><p>Notes:</p><ul><li>Tests ‘filter’ and ‘filter4’ were using Bloom filter to skip strcmp.</li><li>Tests ‘strin-4’ and ‘strin-filter4’ were using flat data structure.</li><li>Tests ‘strcmp1’ and ‘memcmp’ used the strcmp speedup &#39;tricks&#39; described before.</li></ul><h4>Key Takeaway:</h4><blockquote><em>All of those approaches were still comparing strings, one character at a time.</em></blockquote><h3>Stop Comparing Strings</h3><p>Realizing that strcmp <strong>is</strong> the bottleneck, we looked at alternatives. We already observed that all strings are short, have the same length, and fit into a 32-bit integer. So we decided to try to use the <a href="https://en.wikipedia.org/wiki/FourCC">FourCC (4cc) Encoding</a>. The basic idea is to pack 4 bytes into an integer, and replace repeated char compares with a single integer comparison. For example: &quot;USD&quot; becomes 0x00534455 (or 0x41524400 on big-endian architectures).</p><pre>// Before<br>strcmp(s, &quot;USD&quot;)<br><br>// After<br>*(int *) s == 0x00534455</pre><p>The CCY_EQ is now <strong>reinterpreting</strong> the 4-byte strings as an integer:</p><pre>#define CCY_EQ(x, ccy) (*(int *)x == *(int*) ccy )</pre><blockquote><em>This turns each comparison into a single integer load and compare.</em></blockquote><p>Notes:</p><ul><li>On modern X86/X64_86/ARM, it’s OK to fetch an integer via an <em>unaligned</em> pointer — a key enabler for this approach — sometimes with a minor performance cost.</li><li>Possible to write standard-compliant implementation — slightly noisier.</li><li>No explicit conversion needed.</li></ul><p>Even in this basic form, the macro outperforms all previous strcmp-based implementations—without requiring compiler optimization. The raw speed gain from comparing 4cc codes as integers is higher than the gains from optimizing the number of calls to strcmp.</p><p>At this point, we combined the 4cc encoding with the various CCY_IN implementations. This provides better performance vs. the previous CCY_IN that were based on strcmp.</p><h4>Hint from charcmp</h4><p>The latest version was still failing to match the performance of the charcmp approach, where the strcmp was unrolled into a series of single-character comparison. This gave us a hint - <em>unrolling</em>. Our code was using loops for membership tests. Can we combine all the findings from the various tests into a clean, performant implementation?</p><p>We tried:</p><pre>#define CCY_EQ(x, ccy) (*(int *)x == *(int*) ccy )<br><br>#define CCY_EQ0(ccy, x) (x &amp;&amp; CCY_EQ(ccy, x))<br><br>#define CCY_IN_8(ccy, x1, x2, x3, x4, x5, x6, x7, x8, ...) \<br>    CCY_EQ0(ccy, x1) || CCY_EQ0(ccy, x2) || \<br>    CCY_EQ0(ccy, x3) || CCY_EQ0(ccy, x4) || \<br>    CCY_EQ0(ccy, x5) || CCY_EQ0(ccy, x6) || \<br>    CCY_EQ0(ccy, x7) || CCY_EQ0(ccy, x8)<br><br>#define CCY_IN(ccy, ...) CCY_IN_8(ccy, __VA_ARGS__, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)</pre><p>Combining all the findings from the previous benchmarks:</p><ul><li>CCY_IN is unrolling the compare.</li><li>CCY_EQ performs a fast integer comparison.</li><li>The compiler short-circuit the chained-if, when less than 8 values are provided.</li></ul><p>The result: we have clean source conditions: if (CCY_IN(s, &quot;USD&quot;, &quot;EUR&quot;, &quot;JPY&quot;, &quot;GBP&quot;, ...)), that get expanded into efficient and highly optimizable code:</p><pre>int sv = *(int *) s;<br>if ( sv == 0x00445355 || sv == 0x00525545 || sv == 0x0059504A || sv == 0x00504247 || ...)</pre><h4>STR_IN with 4cc encoding:</h4><p>The table below summarizes the results using 4cc encoding. In all cases, comparing 4cc encoded strings outperforms character by character comparison. Combining it with the CCY_IN construct did NOT have negative performance — it actually improves performance — the final code is running 10X faster vs the initial implementation, and 20% faster vs the chained-if approach.</p><pre>GCC                   -Og   -O     -O2    -O3<br>--------------------- ----  ----  -----  -----<br>strcmp (BASELINE)     0.95  1.00   1.00   0.93<br>ccy-21-strin          0.75  0.91   0.91   0.88<br>ccy-31-4cc            4.86  6.12  11.26  10.79<br>ccy-32-4cc-in         2.31  3.90   3.38   3.50<br>ccy-33-4cc-in4        2.36  4.11   3.55   3.53<br>ccy-34-4cc-filter     2.41  4.03   5.08   4.86<br>ccy-35-4cc-filter4    2.43  3.98   5.32   5.63<br>ccy-36-4cc-opt        4.83  6.42  10.50  10.79<br>CLANG<br>ccy-24-strin-memcmp   3.27  2.75   4.80   5.05<br>ccy-31-4cc            8.56  8.46   8.66   8.76<br>ccy-32-4cc-in         3.20  3.50   7.68   8.37<br>ccy-33-4cc-in4        3.59  3.42   9.53  10.18<br>ccy-34-4cc-filter     5.02  4.48   6.66   6.66<br>ccy-35-4cc-filter4    4.03  4.51   6.07   6.49<br>ccy-36-4cc-opt        8.46  8.37   8.66   8.56</pre><h3>What This Taught Me</h3><p>The project provided an opportunity to explore the topic of strcmp performance, which we sometimes treat as a black box. It was also a good experience of trying to balance performance requirements and non-functional requirements (readability, maintainability, auditability):</p><p>On the learning side:</p><ul><li>Start with local optimization, change structure when you hit a wall.</li><li>Clean code and high performance are not always in conflict — sometimes it is possible to achieve both.</li></ul><p>Recap of ideas:</p><ul><li>The strcmp may be expensive, but it does not have to be. When strcmp sits at the core of hot code, it&#39;s worth asking: what are the alternatives? Few options we visited in the article:</li><li>Reduce the number of strcmp calls by performing some comparison at call site. This is extremely effective if most calls are likely to fail.</li></ul><pre>static inline bool CCY_EQ(const char *s1, const char *s2)<br>{<br>    return *s1 == *s2 &amp;&amp; strcmp(s1+1, s2+1) == 0 ;<br>}</pre><ul><li>Fixed size strings are opportunities for performance improvements. Treating those strings as raw data (as opposed to NUL-terminated strings) unlock strategies for performance improvements:</li></ul><blockquote>Consider memcmp to replace strcmp for fixed size strings - the difference is big.</blockquote><ul><li>FourCC (and its big brother EightCC) enable efficient processing of strings (and other data items) — without low-level bit tricks, SIMD wizardry, or hard-to-maintain code. They are simple to implement, and do not require dependency on 3rd party libraries (Reminder: See note about portability).</li></ul><h3>Disclaimer</h3><p>The examples and benchmarks in this article, including linked code snippets, are simplified and reconstructed for illustration purposes. They are not taken from any production system, and do not reflect the design or implementation of any specific codebase.</p><p>This is a personal approach based on general experience working with C codebases. It does not represent any official guideline or the opinion of my employer.</p><p>As with any low-level technique, evaluate carefully before adopting it in production.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b57035b78f18" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/optimizing-chained-strcmp-calls-for-speed-and-clarity-without-refactoring-b57035b78f18">Optimizing Chained strcmp Calls for Speed and Clarity — Without Refactoring</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Safer Casting in C — With Zero Runtime Cost
Making casts visible, auditable, and harder to misuse]]></title>
            <link>https://medium.com/@yair.lenga/safer-casting-in-c-with-zero-runtime-cost-making-casts-visible-auditable-and-harder-to-misuse-331b3a3a8090?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/331b3a3a8090</guid>
            <category><![CDATA[code-review]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[clean-code]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Sun, 05 Apr 2026 14:18:21 GMT</pubDate>
            <atom:updated>2026-04-07T14:08:34.862Z</atom:updated>
            <content:encoded><![CDATA[<h3>Safer Casting in C — With Zero Runtime Cost</h3><h3>Making casts visible, auditable, and harder to misuse</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yFk1r1zm7k4ap-66HLvobA.png" /></figure><p>C gives us two types of casting:</p><ul><li><em>Implicit casting</em> — happens automatically in expressions and function calls</li><li><em>Explicit casting</em> — with the cast operator (T) v (e.g. (int) x)</li></ul><p>Both are powerful — and both introduce risks — making subtle, hard-to-detect bugs easy to introduce.</p><p>The problem is not that casting exists — it’s that <em>it’s too easy to misuse, and too hard to audit.</em></p><h3>Aggressive Explicit Casting</h3><p>The explicit casting will happily convert almost anything into anything else:</p><ul><li>T* to/from T**</li><li>Pointers and integers</li><li>Qualifiers stripped silently.</li></ul><h3>Another problem: cast precedence</h3><p>C casts have very high precedence — higher than most operators. This means they bind tightly to the expression that follows.</p><p>Example:</p><pre>char *p = ... ; <br>long x = *(long *) p + 1;</pre><p>is parsed as</p><pre>long x = (*(long *) p) + 1;                // Option A</pre><p>A developer unfamiliar with exact precedence rules might expect something closer to one of the following:</p><pre>long x = *((long *) (p+1)) ;               // Option B (1 byte forward)<br>long x = *((long *) (p+sizeof(long))) ;    // Option C (next long)</pre><p>These expressions look similar, but behave very differently:</p><ul><li>The first (Option A) reads a long from p, then adds 1</li><li>The second (Option B) reads a long from p+1</li><li>The third (Option C) reads a long from p+sizeof(long)</li></ul><p>Because casts bind tightly, small changes in parentheses can silently change behavior — making such bugs hard to spot in review.</p><h3>Safer Solution ?</h3><p>Compare this to SQL’s CONVERT(type, value) which makes conversions obvious and searchable. In this case one would write:</p><pre>long x = *CAST(long *, p) + 1 ;            // Add one to the long from p<br>long x = *CAST(long *, p+1) ;              // Pick the long starting one byte after p.<br>long x = *CAST(long *, p+sizeof(long)) ;   // Pick the second long from p</pre><p>The goal is not to prevent all invalid casts — but to make incorrect ones fail early, and valid ones easy to audit.</p><h3>A Simple Idea:</h3><p>Replace (T) v with function-like macros that:</p><ul><li>Make casts visible</li><li>Enforce basic correctness at compile time</li><li>Add zero-runtime cost</li></ul><p>This is not “Type-Safe C” — just “harder-to-misuse C”.</p><blockquote><em>This is essentially introducing </em>semantic casts — <em>each cast encodes intent, not just type conversion.</em></blockquote><h3>Example — Implicit conversion bug:</h3><p>The following small program should print the absolute value of the first argument. Calling the wrong absolute value function results in implicit truncation and bad calculation.</p><pre>#include &lt;stdlib.h&gt;<br>#include &lt;stdio.h&gt;<br><br>int main(int argc, char **argv)<br>{<br>        double v = atof(argv[1]) ;<br>        double abs_v = abs(v) ; <br>        printf(&quot;ABS(X)=%f\n&quot;, abs_v) ;<br>}</pre><p>It compiles, but is wrong — abs() expects and returns int, so v is implicitly converted: double-&gt; int -&gt; double. This truncates the fractional part.</p><p>Output — expecting 3.14, getting 3.0</p><pre>./a.out -3.14<br>ABS(X)=3.000000</pre><h3>Structured CAST macros</h3><h4>Design Goals</h4><ul><li>Make casts visible and searchable.</li><li>Catch common mistakes</li><li>Zero runtime cost</li><li>Simple syntax and drop-in usage</li></ul><h4>Proposed API</h4><pre>T CAST(T, v)           // generic entry point<br>T CAST_VAL(T, val)     // scalar / arithmetic values<br>T CAST_PTR(T, ptr)     // any pointer<br>T CAST_PTR1(T, ptr)    // Same as CAST_PTR, limit to single-level pointers<br>UNCONST_PTR(ptr)       // remove qualifiers from the pointee types, which must be const*<br>UNCONST_PTR1(ptr)      // same as UNCONST_PTR, limit to single-level pointers<br>T CAST_CPTR1(T, ptr)   // Same as CAST_PTR1, ptr MUST be const</pre><p>The full header file (~100 lines of code) can be downloaded from <a href="https://gist.github.com/yairlenga/66419da5043ffe1257214c823695a25a">GitHub Gist</a></p><h3>Before / After</h3><pre>/* before */<br>int n = (int) x;<br>char *buf = ... ;<br>long *lp = (long *) buf + 1;<br>free((void *) s);</pre><pre>/* after */<br>#include &quot;safe-cast.h&quot;<br>int n = CAST_VAL(int, x);<br>long *lp = CAST_PTR1(long *, buf) + 1;<br>free(UNCONST_PTR1(s));</pre><p>In practice, the biggest benefit is not just safety — it’s visibility.</p><p>All conversions are now:</p><ul><li>Visible</li><li>Structured</li><li>Easy to grep and audit</li><li>Will fail on “trivial” mistakes</li></ul><h3>API Description</h3><h4>CAST(T, v)</h4><p>Generic entry point. This is the macro to use when the code should read like a normal cast, but still go through the structured API. The main value of CAST is readability: all explicit conversions now go through one visible, searchable API.</p><p>Usually, this is the first step — replace the hidden casts (T) v, with CAST(T, v).</p><p>Example:</p><pre>int x = CAST(int, y);<br>void *p = CAST(void *, buf);</pre><h4>CAST_VAL(T, val)</h4><p>Used for value conversions: integers, floating-point values, enums, booleans, and similar arithmetic cases.</p><p>Example:</p><pre>int n = CAST_VAL(int, d);<br>double x = CAST_VAL(double, count);</pre><p>This macro should reject pointer expressions, so pointer-to-integer or pointer-to-float mistakes do not silently slip through. The macro is similar to C++ static_cast&lt;T&gt;(v), so if your team maintain both C++ and C code, you may even name it STATIC_CAST.</p><h4>CAST_PTR(T, ptr)</h4><p>Used for pointer conversions where the only requirement is that ptr is a pointer expression.</p><p>Example:</p><pre>void *p = CAST_PTR(void *, src);<br>char *s = CAST_PTR(char *, p);</pre><p>This macro is intentionally lightweight. Its main job is to separate pointer casts from value casts, making them easy to audit. It will reject argument which is NOT a pointer. The macro is somewhat similar to C++ reinterpret_cast&lt;T&gt;(p), which allows reinterpretation of raw pointer, so if your team maintains both C++ and C code, you may name it REINTERPRET_CAST.</p><h4>CAST_PTR1(T, ptr)</h4><p>Used for single-level pointers casts only. In other words, ptr must be a T* -style pointer, not T **, and not void *. Example usage will be to convert long *to int *, char * to struct foo *, etc.</p><p>Example:</p><pre>struct foo *buf = ... ;<br>char *s = CAST_PTR1(char *, buf);<br><br>char *work = ... ;<br>struct node *n = CAST_PTR1(struct node *, work);</pre><p>Typical invalid cases:</p><pre>char **pp = ... ;<br>char *p = CAST_PTR1(char *, pp);   /* should fail */</pre><p>This is useful because a common hard-to-detect cast mistake in real code is accidentally mixing T * and T ** (or other level of nesting, array-ness)</p><h4>UNCONST_PTR(ptr)</h4><p>Removes the ‘const’ qualifier from the pointee type of a pointer expression.</p><p>Example:</p><pre>const char *p = get_text();<br>    // We know we can modify the value, get UNCONST_PTR pointer<br>char *q = UNCONST_PTR(p) ;</pre><p>This is intentionally narrow: it is for pointer-to-data cases such as const T * -&gt; T *. It does not try to be a general-purpose “remove const from anything” macro.</p><p>That restriction is intentional. Applying “unconst” to plain values or even structs by value is not useful; it handles the common case when we want to change the mutability of referenced data. Common use case is when a function return a value that must be freed — but we want to treat the value itself is immutable.</p><pre>const char *x = get_value() ;<br>do_something(x) ;<br>free(UNCONST_PTR(x)) ;     // free rejects const *</pre><h4>UNCONST_PTR1(ptr)</h4><p>Explicit form of UNCONST_PTR for the common T * case. This makes the intent even clearer: remove qualifiers from a first-level pointer, not from nested pointers.</p><p>Example:</p><pre>const struct header *h;<br>struct header *mh = UNCONST_PTR1(h);</pre><p>In general, for most cases, we want to use UNCONST_PTR1, which indicate that we expect the pointer to non-mutable object. The macro rejects nested pointers.</p><h4>CAST_UNCONST1</h4><p>The UNCONST_PTR1 and UNCONST_PTR macros depend on C23 typeof_unqual (or the gcc/clang <strong>typeof_unqual</strong>). If those are not available (gcc &lt;=13, clang &lt;=18), possible to use CAST_UNCONST1 which is similar to the pointer CAST_PTR1 macros - with the additional test that ptr points to const object.</p><pre>const char *cp = read_token(...) ;<br>     // We want to update cp<br>char *mut_cp = CAST_UNCONST1(char *, cp) ;<br>mut_cp[0] = &#39;@&#39; ;</pre><h3>Combining with static checkers</h3><p>Beside making the conversion more readable/searchable, it is possible to combine those macros with strict checking by enabling the conversion warnings with gcc/clang. Basic idea:</p><ol><li>compile with gcc/clang -Wconversion, which warns on: Implicit narrowing (long -&gt; int, int-&gt;short), signed/unsigned conversions and float/int.</li><li>Review each conversion. Fix broken conversions, add explicit conversions with CAST, CAST_PTR1, UNCONST_PTR1 as needed.</li><li>Raise conversion to warning to errors with -Werror=conversion</li></ol><p>At this point, any code changes that result in implicit or illegal conversions (as per Macro restrictions) will be flagged as an error — preventing unexpected surprises at run time.</p><p>For even stronger validation — consider enabling gcc/clang cast related warnings (-Wcast-qual, -Wcast-align, ...). You can also get additional mileage using gcc/clang analyzer, and additional tools like clang-tidy or commercial tools like coverity.</p><h3>Implementation</h3><p>Each of the macro is implemented with an expression that will force compile time checks, with zero run-time checks. Because “C” does not have standard for some of the checks (e.g. is_pointer) — we are using compiler extension that exists in gcc/clang. If you use other compilers that do not support gcc/clang extensions — It’s possible to implement <em>SOME</em> of the restrictions with C23.</p><p>It’s important to highlight one aspect of those macro — they capture the <em>intended usage of the cast</em>. While current implementation/compilers might not be able to fully enforce the restriction — future implementation and compiler versions might include new extensions, and future updates to the C standard might support stronger enforcement.</p><p>You can retrieve implementation of those macro for gcc 13, and clang 18, that I validated on Ubuntu 2024 (under WSL) from GITHUB gist, and drop it into your code base. single header file.</p><p>Below is short description for the implementation of CAST, CAST_VAL. Other macros implementation follow the same line.</p><h4>Implementing the basic CAST</h4><pre>#define CAST(T, v) ((T) v)</pre><h4>Implementing the CAST_VAL(T, val)</h4><p>The implementation of all macros that enforce restriction on the converted value is to use 2 expressions:</p><ul><li>The first expression enforce the restriction at compile time, but has no run-time effect.</li><li>The second expression perform the conversion, using the basic CAST</li></ul><pre>#define CAST_VAL(T, v) (CAST_REQUIRE_VALUE(v), CAST(T, v))<br><br>static inline void cast_require_value(double v) { (void) v; }<br><br>#define CAST_REQUIRE_VALUE(v) ((void)sizeof(cast_require_value(v)))</pre><p>The cast_require_value function does nothing - but it will only accept double value. In &quot;C&quot;, scalar numeric values - integer, floating-point, enum, ... will be promoted to double as needed. Therefore, the call to cast_require_value will succeed if a value is passed, and will fail with non-value - specifically - pointers, unions, structures, ...</p><p>We mentioned that we want ZERO run-time effect. This is achieved by applying the sizeof of the restriction. Per C language rules - sizeof does NOT evaluate the expression - it calculate (at compile time) the size (both gcc and clang evaluate sizeof(void) to 1).</p><h3>Summary</h3><p>C casting is extremely powerful — and that’s exactly the problem. Both implicit and explicit casts can silently introduce bugs that are hard to detect and even harder to trace.</p><p>This article proposes a simple approach:</p><ul><li>Replace (T) v with function-like macros</li><li>Make conversions visible and searchable</li><li>Catch common mistakes at compile time</li><li>Keep zero runtime cost</li></ul><p>The goal is not to make C “type-safe”, but to make casting:</p><ul><li>Explicit</li><li>Auditable</li><li>Harder to misuse</li></ul><p>Some of the biggest improvements in C code don’t come from new language features — but from better discipline, encoded in small reusable patterns.</p><h3>Caveats and Limitations</h3><p>This approach is intentionally lightweight and pragmatic, but it comes with a few important caveats:</p><h4>Compiler support</h4><p>The implementation relies on GCC/Clang extensions (e.g. type checks via expressions that trigger compile-time errors).<br>It has been tested with GCC 13, 14 and Clang 18, 19, but may not work — or may require adjustments — on other compilers.</p><h4>Not a complete type system</h4><p>These macros do not make C type-safe.<br>They only enforce a limited set of structural constraints (e.g. pointer vs value, pointer depth).</p><p>Incorrect casts are still possible — but many common mistakes become compile-time errors.</p><h4>Error messages</h4><p>Some invalid uses will produce compiler errors that are not always intuitive, especially when triggered through macro expansion.</p><p>This is a trade-off for zero runtime cost and portability.</p><h4>Requires discipline</h4><p>The benefits come from <strong>consistent usage</strong>:</p><ul><li>Replace (T)v with CAST(...)</li><li>Enable strict warnings (-Wconversion, -Werror)</li></ul><p>Partial adoption reduces effectiveness.</p><h4>Test before adoption</h4><p>This approach should be validated in your codebase:</p><ul><li>Check compatibility with your compiler/toolchain</li><li>Evaluate error messages and developer experience</li><li>Ensure it integrates well with existing coding guidelines</li></ul><h3>Disclaimer</h3><p>This is a personal approach based on practical experience working with C codebases.<br>It does not represent any official guideline or the opinion of my employer.</p><p>As with any low-level technique, evaluate carefully before adopting it in production.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=331b3a3a8090" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Temporary Memory Isn’t Free: Allocation Strategies and Their Hidden Costs]]></title>
            <link>https://blog.stackademic.com/temporary-memory-isnt-free-allocation-strategies-and-their-hidden-costs-159247f7f856?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/159247f7f856</guid>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[memory-management]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Mon, 30 Mar 2026 12:08:24 GMT</pubDate>
            <atom:updated>2026-03-30T12:08:24.583Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ygcc2t44ISMyFIUaXgEazQ.jpeg" /></figure><p>This article completes a short series on temporary memory in C:</p><ul><li>First, we introduced stack-based allocation (using VLA) as a practical alternative to malloc for short-lived data</li><li>Then, we showed how to safely estimate and manage available stack space</li><li>Here, we measure the payoff: how allocation strategy impacts performance in a realistic workload</li></ul><h3>Introduction</h3><p>In many discussions, memory allocation is treated as an O(1) operation — a constant-time primitive that can be safely ignored in performance-critical code. In practice, that constant can be surprisingly large. Allocating memory may involve managing free lists, splitting or merging blocks, or occasionally requesting additional pages from the operating system. These costs are usually hidden behind a fast path, but when allocation happens repeatedly inside tight loops, the “constant time” assumption starts to leak.</p><p>This becomes particularly relevant in financial analytics, where temporary memory is not just a convenience but often a requirement. Models are typically structured as independent components, each producing intermediate results that must be preserved for auditability, explainability, or reuse in downstream calculations. A typical valuation may generate multiple time series — such as scheduled principal, prepayments, interest, and losses — which are then aggregated or fed into other models. This separation improves modularity and traceability, but it also means that even relatively simple calculations rely on temporary arrays that are allocated and discarded repeatedly.</p><p>While this article uses VLA as a concrete mechanism, the underlying point is broader: stack-based allocation has fundamentally different performance characteristics than heap allocation.</p><p>As a result, allocation patterns that might seem avoidable in theory become common in practice — especially when applied across large portfolios, where these temporary structures are created thousands or millions of times.</p><h3>A Simple Use Case: Loan Portfolio PV Calculation</h3><p>A typical loan valuation computes cashflows over a time grid (often daily or monthly) from origination to maturity. For each time step, the model derives scheduled principal, interest, and expected losses based on the current balance and assumptions such as prepayment and default rates. These values are usually stored in arrays, both to support multi-pass calculations (e.g., aggregation, stress adjustments) and to provide a full audit trail of intermediate results. The discounted present value is then obtained by applying a corresponding discount factor curve to each time step.</p><h3>Per-loan processing</h3><p>Each loan is evaluated independently over a time grid (daily, using a simplified 30/360 convention) from origination to maturity. At each step, the model updates the outstanding balance and computes the corresponding cashflows based on contractual terms and behavioral assumptions (e.g., prepayments, defaults).</p><h3>Arrays over time: S, I, U, L, DF</h3><p>The calculation typically materializes several time series, coming from different models.</p><ul><li>S[t] — Scheduled principal payments - Cash Flow model</li><li>I[t] — interest payments - Cash Flow model</li><li>U[t] — Unscheduled payments (pre-pays) - Prepay model</li><li>L[t] — losses (defaults / write-offs) - Loss model.</li><li>DF[t] — discount factors - Interest rate model</li></ul><p>These arrays are indexed by time and span the full horizon of the loan. In our example, we will cap the horizon to 50 years.</p><h3>Temporary workspace per loan</h3><p>Even when only the final present value is required, intermediate results are usually stored in arrays. This supports:</p><ul><li>multi-pass calculations (generation → aggregation → adjustments)</li><li>scenario or stress overlays</li><li>auditability and explainability of results</li></ul><p>In practice, this means allocating a working set proportional to the number of time steps for each loan.</p><h3>A Reasonable Implementation</h3><p>This implementation is representative of real-world code:</p><ul><li>Each loan is processed independently</li><li>Temporary arrays (S, I, U, L, DF) are allocated per loan</li><li>Discount factors are computed once and reused</li></ul><p>Nothing here looks unusual or inefficient — and that’s exactly the point. The expectation is that allocation overhead is small compared to the numerical work.</p><p>This assumption is what we test next.</p><pre>static struct portfolio_result<br>port_pv_heap_per_loan(int loans, int sim_days)<br>{<br>    struct portfolio_result res = {0};<br>    double *DF = xmalloc(STATIC_MAX_DAYS * sizeof(*DF));<br>    calc_DF(sim_days, DF, 5.0);<br>    for (int loan = 0; loan &lt; loans; ++loan) {<br>        struct loan_info info = get_loan_info(loan, sim_days);<br>        int loan_days = info.days;<br>        double *U = xmalloc(loan_days * sizeof(*U));<br>        double *S = xmalloc(loan_days * sizeof(*S));<br>        double *I = xmalloc(loan_days * sizeof(*I));<br>        double *L = xmalloc(loan_days * sizeof(*L));<br>        model_loan(&amp;info, S, U, I, L);<br>        res.pv += loan_pv(loan_days, S, U, I, L, DF);<br>        free(L);<br>        free(I);<br>        free(P);<br>        free(S);<br>    }<br>    free(DF);<br>    return res;<br>}</pre><h3>The loan modeling code</h3><p>The modeling logic itself is straightforward and operates over the provided workspace:</p><pre>static void model_loan(<br>    const struct loan_info *loan,<br>    double *S,<br>    double *P,<br>    double *I,<br>    double *L<br>) {<br>    // Setup<br>    double bal = ...<br>    // Cash flows<br>    for (int t = 0; t &lt; loan-&gt;days; ++t) {<br>        // Calculate scheduled_principal, prepay, interest and losses<br>        S[t] = scheduled_principal(...) ;<br>        P[t] = prepay(..);<br>        I[t] = interest(...);<br>        L[t] = loss(...);<br>    }<br>}</pre><p>Actual code (single file, build instruction on the top) available as <a href="https://gist.github.com/yairlenga/526270872b992efb56ee6bb6666b3ccb">GitHub Gist</a>.</p><h3>Allocation Strategies</h3><p>To understand the impact of allocation, we compare several strategies that differ only in how temporary memory is managed. The computation itself is identical in all cases.</p><h3>Static reusable buffers (reference)</h3><p>A single set of static buffers is allocated once and reused across all loans.</p><p>This approach has effectively <strong>zero allocation overhead</strong> during the benchmark and serves as the reference point. It represents the best-case scenario where memory management is fully amortized and removed from the hot path.</p><h3>Stack allocation (VLA and fixed-size)</h3><p>Temporary arrays are allocated on the stack per loan, either using fixed-size arrays or Variable Length Arrays (VLA).</p><p>This avoids heap allocation entirely and keeps allocation cost very low and predictable. However, it is constrained by stack size and may require safeguards for large problem sizes.</p><h3>Heap allocation per loan (malloc / free)</h3><p>Each loan allocates its own working arrays using malloc and releases them after processing.</p><p>This is the most straightforward and modular approach, but it introduces allocation overhead directly into the hot loop and stresses the allocator under repeated use.</p><h3>Heap reuse (per portfolio)</h3><p>Buffers are allocated once per portfolio (or per thread) and reused across loans. Per-loan data is allocated to the maximum possible size.</p><p>This removes most allocation overhead while preserving flexibility. It is a common compromise in performance-sensitive systems, but makes the code less modular — callers must anticipate workspace requirements.</p><h3>Bulk allocation</h3><p>A single large block is allocated and partitioned into the required arrays (S, P, I, L, DF).</p><p>This reduces the number of allocation calls and improves locality, but still relies on the heap allocator and may incur setup cost.</p><p>In all cases, the only difference is how memory is obtained and released.<br>This allows us to isolate the cost of allocation itself.</p><h3>Benchmark Design</h3><p>To capture allocator behavior across realistic environments, we run the same benchmark under several common compiler and allocator combinations:</p><ul><li><strong>GCC + glibc (default Linux)</strong><br>Baseline configuration used in most production Linux systems.</li><li><strong>Clang + glibc</strong><br>Same allocator, different compiler — highlights code generation effects independent of allocation.</li><li><strong>GCC + musl</strong><br>Lightweight allocator commonly used in containers (e.g., Alpine); known for different performance trade-offs.</li><li><strong>GCC + mimalloc</strong><br>Modern allocator optimized for fast paths and low fragmentation, widely used in performance-sensitive systems.</li><li><strong>GCC + jemalloc</strong><br>Mature allocator with strong scalability and fragmentation control, used in databases and large-scale services.</li><li><strong>GCC + tcmalloc</strong><br>Google’s allocator, optimized for high-throughput multi-threaded workloads.</li></ul><p>All tests are run in release mode with optimizations enabled, with minimal run-time checks. No debugging, tracing, or instrumentation features are active in any allocator — using default “out-of-the-box” setting.</p><p>All measurements are performed in a single-threaded application to keep the analysis focused and comparable. This isolates allocation costs without introducing contention or synchronization effects. In multi-threaded workloads, allocator behavior becomes more complex, and additional overheads — such as thread-local cache management, cross-thread frees, and synchronization — can introduce further runtime penalties. As a result, the single-threaded results presented here should be viewed as a lower bound on allocation cost.</p><h3>Results</h3><p>The choice of allocation strategy has a measurable impact on performance — even for relatively simple computations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/989/1*XTjTdvo3A2iFde8bakeFJw.png" /></figure><p>In our benchmark, per-loan allocation using malloc is up to <strong>2.5x slower</strong> than reusing memory, while stack-based approaches (VLA) remain close to the optimal baseline. When using &quot;simple&quot; allocators (musl) the cost of using malloc can be as high as <strong>6x slower</strong>.</p><h3>Throughput</h3><p>The following table summarizes relative throughput, when running the simulation on a portfolio of 1000 loans with durations between 3000 and 12000 days (approximately 9 and 33.5 years). Average result, as reported by the program over 10 runs each.</p><p>To replicate ./alloc-bench 0 1000 12000</p><p><em>Normalized to static = 100% (higher is better)</em></p><pre>Compiler →  |                  gcc                      |  clang<br>Allocator → | glibc  jemalloc  mimalloc  musl  tcmalloc |  glibc<br>----------------------------------------------------------------<br>static      | 100%     100%      100%    100%     100%  |   100%<br>heap/bulk   | 101%      96%       97%     19%      99%  |    99%<br>heap/loan   |  28%      98%       83%     16%      82%  |    44%<br>heap/reuse  |  97%     102%       85%     89%      84%  |   100%<br>vla         |  96%     101%       98%     99%      98%  |   101%</pre><p>The clang/glibc result for per-loan allocation stands out and may reflect differences in code generation or allocator interaction.</p><p>Additional runs (including shorter durations) show similar patterns/trends and are available in this <a href="https://gist.github.com/yairlenga/45fba4d746eedfba2d915d754351621b">GitHub Gist</a>.</p><h3>Key Observations</h3><p>Several patterns stand out from the results:</p><ul><li><strong>Stack allocation remains consistently close to the theoretical limit</strong><br>Across all configurations, stack allocation (fixed-size arrays and VLA) performs very close to the static baseline — effectively the lower bound where allocation cost is eliminated.</li><li><strong>Allocators are tuned, not universal</strong><br>Each allocator performs well under certain allocation patterns, but it is easy to fall outside its “comfort zone.” — even in otherwise reasonable implementations. Repeated allocation/free in tight loops can expose slow paths and lead to significant performance penalties.</li><li><strong>Simple allocators can struggle under pressure</strong><br>The musl allocator, while lightweight and predictable, shows the weakest performance in allocation-heavy scenarios. Its simplicity becomes a disadvantage when faced with frequent, repeated allocations. This is particularly relevant in cloud environments (e.g., Alpine-based containers), where lightweight allocators like musl are common.</li><li><strong>Optimized allocators still have weak spots</strong><br>More sophisticated allocators (mimalloc, jemalloc, tcmalloc) generally perform better, but not uniformly. Some allocation patterns are handled almost for free, while others incur noticeable overhead. Performance is highly pattern-dependent.</li><li><strong>The compiler matters too</strong><br>Even with the same allocator (glibc), compiler choice has an impact. Clang shows slightly better and more stable performance compared to GCC, suggesting that code generation and optimization influence how allocation costs are expressed.</li><li><strong>Variability increases with allocation size</strong><br>As allocation sizes grow — especially near thresholds where allocators switch to mmap — runtime becomes more variable. This reflects transitions between fast-path allocation and slower OS-backed paths, introducing both latency and unpredictability.</li></ul><p>Overall, allocation cost is not just about the allocator — it is the interaction between allocator, allocation pattern, and compiler.</p><h3>Caveats</h3><ul><li>Results are from a single-threaded benchmark; multi-threaded workloads may introduce additional allocator overhead</li><li>Stack-based approaches (VLA) are limited by available stack size and may require safeguards for large inputs</li><li>The model materializes intermediate arrays for clarity and auditability; other designs may reduce allocation at the cost of complexity</li></ul><h3>Practical Takeaways</h3><p>The performance upside is not academic.</p><p>In simulation-heavy workloads — financial models, risk scenarios, Monte Carlo, or any system that repeatedly builds temporary state — allocation sits directly in the hot path. Small per-call overheads accumulate quickly, and differences of 2–3× at the allocation level can translate into meaningful end-to-end impact.</p><p>Stack-based allocation (fixed-size and VLA) offers a simple way to approach the lower bound: allocation cost that is effectively constant and close to zero. VLA provides a practical mechanism to apply this pattern to size-dependent data.</p><p>Used responsibly, VLA does not have to be an all-or-nothing choice. A common pattern is to use a <strong>conditional approach</strong>:</p><ul><li>allocate on the stack for small, bounded sizes</li><li>fall back to heap allocation for larger inputs</li></ul><p>This provides predictable performance while avoiding stack overflow risks.</p><p>It is certainly possible to tune allocator behavior, adjust parameters, or carefully shape allocation patterns. However, these approaches add complexity and are often allocator-specific. In many cases, a simple conditional VLA/heap strategy achieves comparable or better results with significantly less effort.</p><h4>To Summarize:</h4><blockquote><em>If temporary memory sits in your hot path, how you allocate it matters — and simple strategies can go a long way.</em></blockquote><blockquote><em>Avoiding allocation is often the best optimization.<br>When that’s not possible, controlling it explicitly is the next best thing.</em></blockquote><p>A follow-up article will present a small library that encapsulates these patterns, making stack-based and conditional allocation easier to apply in real code.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=159247f7f856" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/temporary-memory-isnt-free-allocation-strategies-and-their-hidden-costs-159247f7f856">Temporary Memory Isn’t Free: Allocation Strategies and Their Hidden Costs</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Missing Metric on Medium: Article-to-Article Readership Overlap]]></title>
            <link>https://medium.com/@yair.lenga/a-missing-metric-on-medium-article-to-article-readership-overlap-5be42abcf2be?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/5be42abcf2be</guid>
            <category><![CDATA[medium]]></category>
            <category><![CDATA[writing]]></category>
            <category><![CDATA[analytics]]></category>
            <category><![CDATA[writing-on-medium]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Sat, 21 Mar 2026 21:23:50 GMT</pubDate>
            <atom:updated>2026-03-21T21:23:50.221Z</atom:updated>
            <content:encoded><![CDATA[<p>Medium gives good per-article stats: views, reads, read ratio. But once you publish more than one article, an important question becomes impossible to answer:</p><blockquote>Are the same people reading my articles, or am I reaching entirely new audiences each time?</blockquote><h3>The gap</h3><p>Right now, stats are <strong>per article only. </strong>I’m not aware of away to understand how two articles relate in terms of readership.</p><p>For example, if I publish:</p><ul><li>Article A (stack allocation)</li><li>Article B (allocation strategies)</li></ul><p>I want to know:</p><ul><li>Did readers of A also read B?</li><li>Did B reach a different audience?</li><li>Am I building depth, or just breadth?</li></ul><h3>Proposal: a 3×3 readership matrix</h3><p>For any pair of articles, show a matrix like this:</p><pre>Article B:<br>        Article A:  Read. View. None<br>   read:            ??    ??   ??<br>   view:            ??    ??   ??<br>   none:            ??    ??   ??</pre><p>Where</p><ul><li><strong>Read</strong> = member read (as defined by Medium)</li><li><strong>View = </strong>opened but not read</li><li><strong>None = </strong>did not see the article</li></ul><h3>Why this matters</h3><p>This single matrix answers several high-value questions:</p><h4>1. Audience overlap</h4><ul><li>High (Read A, Read B) → strong continuity</li><li>Low overlap → different audiences</li></ul><h4>2. Funnel insight</h4><ul><li>(View A, Read B) → second article performs better</li><li>(Read A, View B) → drop-off or weaker hook</li></ul><h4>3. Content strategy</h4><ul><li>Are follow-up articles actually reaching the same readers?</li><li>Are readers exploring your profile, or just reading one piece?</li></ul><h3>4. Series validation</h3><p>For multi-part topics:</p><ul><li>Is the audience progressing through the series?</li><li>Or are parts disconnected?</li></ul><h3>Why this is more useful than claps</h3><p>Claps and followers are coarse signals.</p><p>This matrix shows <strong>reader behavior across content</strong>, which is far more actionable:</p><ul><li>Should I write a continuation?</li><li>Should I reframe the topic?</li><li>Are my articles connected or isolated?</li></ul><h3>Implementation notes (high-level)</h3><ul><li>This can be computed from existing Medium data</li><li>Only aggregate counts are needed (no privacy concerns)</li></ul><p>Could be limited to:</p><ul><li>author’s own articles</li><li>last N articles</li><li>optional time window</li></ul><h3>Example insight</h3><p>If I see:</p><ul><li>High (Read A, None B)</li><li>Low (Read A, Read B)</li></ul><p>Then:</p><blockquote>My second article is not reaching my existing readers</blockquote><p>That’s something I cannot detect today.</p><h3>Closing</h3><p>Medium already helps writers understand <strong>individual articles</strong>. This would help us understand <strong>relationships between articles — </strong>which is where real content strategy lives.</p><p>Curious if others would find this useful.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5be42abcf2be" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Much Stack Space Do You Have? Estimating Remaining Stack in C on Linux]]></title>
            <link>https://medium.com/@yair.lenga/how-much-stack-space-do-you-have-estimating-remaining-stack-in-c-on-linux-3c9513beabd8?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/3c9513beabd8</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[memory-management]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[low-level-programming]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 14:06:00 GMT</pubDate>
            <atom:updated>2026-03-18T01:49:17.935Z</atom:updated>
            <content:encoded><![CDATA[<h3>Practical techniques for estimating remaining stack space at runtime on Linux systems.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9lQTq4lmPZ1NDPKgBmXIXA.png" /></figure><p>In a previous article (<a href="https://medium.com/@yair.lenga/avoiding-malloc-for-small-strings-in-c-with-variable-length-arrays-vlas-7b1fbcae7193">Avoiding malloc for Small Strings in C With Variable Length Arrays (VLAs)</a>) I suggested using <strong>stack allocation (VLAs)</strong> for small temporary buffers in C as an alternative to malloc().</p><p>One of the most common concerns in the comments was:</p><blockquote>“Stack allocations are dangerous because you cannot know how much stack space is available.”</blockquote><p>This concern is understandable. If a program accidentally exceeds the stack limit, the result is usually a <strong>segmentation fault</strong>.</p><p>While the C language and standard library do not expose stack information, modern operating systems — including Linux — expose enough information to <strong>estimate available stack space</strong>.</p><p>This article explores a few practical techniques to answer the question:</p><blockquote><strong><em>How much stack space does my program have left?</em></strong></blockquote><p>The goal is not perfect precision, but <strong>good enough estimates</strong> to guide decisions such as whether to allocate memory on the stack or the heap.</p><h3>The Stack on Modern Linux</h3><p>On most modern Linux systems the default stack size for a process is typically around <em>8 MB</em>. You can confirm this using ulimit, which will report the result in 1024 byte units.</p><pre>$ ulimit -s<br>8192</pre><p>On modern Linux platforms (X86–64, ARM, RISC-V, PowerPC) the stack grows <strong>downward in memory</strong>, meaning that as functions are called and local variables are allocated, the stack pointer moves toward lower addresses.</p><pre>Higher addresses<br>┌───────────────────────────┐<br>│  stack start / top        │<br>│  main() frame             │<br>│  caller frames            │<br>│  local variables          │<br>│  current stack pointer    │<br>│                           │<br>│  unused stack space       │<br>│                           │<br>│  stack limit (guard page) │<br>└───────────────────────────┘<br>Lower addresses</pre><p>When the stack grows beyond the guard page, the operating system raises a segmentation fault.</p><p>To estimate remaining stack space we need two pieces of information:</p><ol><li><strong>Stack boundaries</strong> (base and size)</li><li><strong>Current stack pointer</strong></li></ol><p>Once we know those, the remaining stack can be approximated by measuring the distance between them.</p><pre>stack_base = lowest stack address<br>stack_top  = highest stack address<br>stack_remaining = current_stack_pointer - stack_base<br>stack_inuse = stack_base + stack_size - current_stack_pointer</pre><h3>Getting the Current Stack Pointer</h3><p>C does not provide an official API to query the stack pointer.</p><p>In practice, the address of a <strong>local variable</strong> provides a very good approximation of the current stack position, since local variables are typically stored in the current stack frame.</p><p>Example:</p><pre>static StackAddr stack_marker_addr(void)<br>{<br>    char marker;<br>    return (StackAddr) &amp;marker;<br>}</pre><p>This address is usually close to the current position of the stack pointer.</p><p>When compiling with higher optimization levels, the compiler may rearrange stack layout or inline helper functions in ways that make the measurement less predictable. To reduce this effect, it helps to:</p><ul><li>take the address of a volatile local variable</li><li>place the logic in a noinline helper function</li></ul><p>A small helper like the following works well in practice:</p><pre>[[gnu::noinline]]<br>static StackAddr stack_marker_addr(void)<br>{<br>    volatile char marker;<br>    return (StackAddr) &amp;marker ;<br>}</pre><p>The next step is to get the stack boundaries so that we can estimate the remaining (and inuse) stack space. For the remaining stack space we need the lower address of the stack (stack_base). There are several ways to obtain this address. We will cover:</p><ul><li>Method 1: Query the Stack Limit with getrlimit</li><li>Method 2: Using pthread_getattr_np</li><li>Method 3: Capturing the Stack Position at Program Startup</li></ul><h3>Method 1: Query the Stack Limit with `getrlimit</h3><p>Linux exposes the maximum stack size through the getrlimit() system call.</p><pre>const char *get_stack_base(void)<br>{<br>    struct rlimit stack_limit ;<br>    getrlimit(RLIMIT_STACK, &amp;stack_limit) ;<br>    stack_size = stack_limit.rlim_cur ;<br>    // get stack_top from stack_marker_addr<br>    stack_base = stack_top - stack_limit.rlim_cur ;<br>    return stack_base ;<br>}</pre><p>This returns the <strong>maximum stack size</strong> configured for the process.</p><p>By capturing the stack pointer <strong>early</strong> in the program and combining it with maximum stack size, we can estimate the stack base, and the remaining stack space:</p><p>Conceptually:</p><pre>stack_top = stack_marker_addr()      // at program start.<br>stack_size = ... // from getrlimit<br>stack_base = stack_top - stack_size</pre><p>This method is simple and portable across many Linux systems, but it has few limitations, in particular: <strong>It requires capturing the stack position early in the program to establish a reference point.</strong></p><p>If the first opportunity to capture the stack address occurs after significant stack allocations have already occurred, we might over-estimate the remaining stack space as there is no easy way to estimate the space already been used. In those cases, an alternative method exists.</p><p>Complete Implementation (build instruction in comments) as GitHub GIST</p><p>Note that RLIMIT_STACK gives the maximum allowed stack, not necessarily the mapped stack. The actual stack memory is usually grown lazily by the kernel as needed.</p><p>See working example (<a href="https://gist.github.com/yairlenga/b24d0f6feebc403f38f7df90ee8aecc1">gist-2603-stack-getrlimit.c</a>)</p><h3>Method 2: Using pthread_getattr_np</h3><p>Linux systems using glibc provide a convenient non-standard extension, pthread_getattr_np(), which allows a thread to query its own stack attributes, including the stack base address and stack size.</p><p>Example Usage:</p><pre>pthread_attr_t attr ;<br>    void *stack_base ;<br>    size_t stack_size ;<br>    pthread_getattr_np(pthread_self(), &amp;attr) ;<br>    pthread_attr_getstack(&amp;attr, &amp;stack_base, &amp;stack_size) ;</pre><p>From this we can obtain the stack_base, which can now use for estimating the remaining stack, and inuse stack, as discussed above.</p><p>This method has several advantages:</p><ul><li>Works <strong>in multi-threaded programs</strong> (different threads may have different stack size!)</li><li>Does not require change to program startup</li><li>Provides <strong>direct access to stack boundaries</strong></li></ul><p>For Linux programs that already use pthread, this is often the <strong>cleanest approach</strong>. Using this technique on single threaded program requires the program to link with the pthread library, but <strong>does not</strong> launch extra threads, or introduce thread-safety issues into code that does not otherwise launch additional threads.</p><p>See working example (<a href="https://gist.github.com/yairlenga/3d2e098d83099e889161c449f7988244">gist-2603-stack-pthread.c</a>)</p><h3>Method 3: Capturing the Stack Position at Program Startup</h3><p>The previous method uses pthread_getattr_np() to query stack boundaries directly. While convenient, it requires linking with the pthread library and relies on a non-standard GNU extension.</p><p>In many programs/libraries, especially single-threaded utilities, it may be desirable to estimate stack usage <strong>without introducing a dependency on pthread</strong>.</p><p>One simple technique is to capture the stack position <strong>very early in the program’s lifetime</strong>, before additional call frames are created. On systems using GCC or Clang this can be done using a <em>constructor function</em>.</p><p>Functions marked with the constructor attribute are executed automatically before main(). The attribute is commonly used by runtime libraries (including C++ runtimes) to perform initialization before main. It can also be used in C programs/functions.</p><p>Example:</p><pre>static StackAddr stack_base;<br>static size_t stack_size ;<br><br>__attribute__((constructor))<br>static void capture_stack_region(void)<br>{<br>    StackAddr stack_top = stack_marker_addr() ;<br>    struct rlimit stack_limit ;<br>    getrlimit(RLIMIT_STACK, &amp;stack_limit) ;<br>    stack_size = stack_limit.rlim_cur ;<br>    stack_base = (StackAddr) stack_top - stack_limit.rlim_cur ;<br>}</pre><p>Because this function runs during program startup, the recorded address is typically very close to the <strong>top of the initial stack</strong>. Combining this address with the configured stack size provides a good approximation of the stack base.</p><p>Later in the program we can compare this value with the current stack position to estimate stack usage, and remaining stack</p><pre>size_t stack_space = stack_marker_addr() - stack_base ;</pre><p>This approach avoids the need for pthread, and the measurement can be implemented entirely inside a helper module without requiring any changes to main().</p><p>See working example (<a href="https://gist.github.com/yairlenga/0a60a1d071808035ff75f88570be6e62">gist-2603-stack-constructor.c</a>)</p><p>Like the other techniques presented here, this method provides an <strong>estimate</strong> rather than an exact measurement, but it is often sufficient to guide decisions such as whether a temporary buffer should be placed on the stack or the heap.</p><h3>Turning This into a Small Utility</h3><p>Once the stack boundaries are known, it is easy to wrap the calculation into a small helper functions. The stack_remaining helper also tracks the lowest observed stack address to estimate max usage of stack space.</p><p>Conceptually:</p><pre>static size_t stack_remaining(void)<br>{<br>    StackAddr sp = stack_marker_addr() ;<br>    if ( sp &lt; stack_low_mark ) stack_low_mark = sp ;<br>    return sp - stack_base - safety_margin;    <br>}</pre><pre>static size_t stack_inuse(void)<br>{<br>    return stack_base + stack_size - stack_marker_addr()<br>}</pre><h3>A Small Stack Inspection Utility</h3><p>Note that the stack_remaining also tracks the lowest stack marker. This will allow us to expose &quot;stack_info&quot; Similar in spirit to &quot;mallinfo&quot;, with the following attributes:</p><pre>struct stack_info {<br>    StackAddr base ;<br>    size_t size ;<br>    size_t max_inuse ;<br>    size_t margin ;<br>    StackAddr low_mark ;<br>    ...<br>}<br>struct stack_info get_stack_info(void) ;</pre><h3>Using Stack Estimates to Guide Allocation Decisions</h3><p>The practical motivation for estimating stack space is simple:</p><p>Some allocations are small enough that placing them on the stack is faster and simpler than using the heap.</p><p>However, we want to avoid risking stack overflow.</p><p>A simple strategy is to allocate on the stack <strong>only when sufficient space remains</strong>.</p><p>Example logic for a function that needs double[n] temporary storage.</p><pre>function foo(int n, double x)<br>{<br>    size_t need_mem = n * sizeof(double) ;<br>    bool use_vla = need_mem &lt; stack_remaining() ;<br>    double y_vla[use_vla ? n : 1] ;<br>    double *y = use_vla ? y_vla : malloc(need_mem) ;<br>    // Use y as needed<br>    // Cleanup<br>    if ( !use_vla ) free(y) ;<br>}</pre><p>This allows the program to use the stack when it is safe — avoid malloc calls and fall back to the heap otherwise.</p><h3>How Accurate Are These Estimates?</h3><p>These methods provide <strong>estimates</strong>, not guarantees.</p><p>A few factors can influence stack usage:</p><ul><li>deep call stacks</li><li>recursion</li><li>large local variables</li><li>compiler optimizations</li><li>thread stack sizes</li></ul><p>Because of this, it is wise to leave a <strong>safety margin</strong> when making decisions based on remaining stack space.</p><p>In practice, leaving a few kilobytes (8–32) of buffer is usually sufficient. The sample code allocate 2 pages.</p><h3>Decision Chart</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3psYRoTn9KxfY3dnFqkFKA.png" /></figure><h3>Conclusion</h3><p>Although the C language itself does not expose stack information, modern Linux systems provide enough primitives to estimate stack usage.</p><p>Using APIs and features such as:</p><pre>getrlimit()<br>pthread_getattr_np()<br>GCC/CLANG constructor attribute.</pre><p>a program can determine stack limits and approximate the remaining stack space at runtime.</p><p>This does not eliminate the need for careful programming, but it does make stack allocation decisions <strong>far more informed than commonly assumed</strong>.</p><p>In a follow-up article we will explore a more experimental approach: <strong>actively probing the stack itself to discover its limits</strong>.</p><h3>Disclaimer</h3><p>The views expressed in this article are my own and do not necessarily reflect those of my employer.</p><p>Some of the code examples in this article were generated with the assistance of AI tools and have not been tested in production environments. They are provided for illustration and experimentation only.</p><p>This article focuses on practical techniques for Linux systems and does not attempt to provide a portable or fully general solution.</p><p>The code and techniques described in this article are provided for educational purposes only and are not guaranteed to be correct or suitable for production use. The author makes no warranties regarding accuracy or fitness for any particular purpose.</p><p>If this article was useful, please clap so other C developers can find it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3c9513beabd8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Avoiding malloc for Small Strings in C With Variable Length Arrays (VLAs)]]></title>
            <link>https://blog.stackademic.com/avoiding-malloc-for-small-strings-in-c-with-variable-length-arrays-vlas-7b1fbcae7193?source=rss-2c0c4950e7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/7b1fbcae7193</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[memory-management]]></category>
            <category><![CDATA[linux-system-programming]]></category>
            <category><![CDATA[c-programming]]></category>
            <category><![CDATA[preformance]]></category>
            <dc:creator><![CDATA[Yair Lenga]]></dc:creator>
            <pubDate>Tue, 10 Mar 2026 13:01:02 GMT</pubDate>
            <atom:updated>2026-03-19T07:33:35.399Z</atom:updated>
            <content:encoded><![CDATA[<h4>A simple stack-first buffer technique that reduces heap allocations</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4YpfXoESzugadxUXx3fYzg.png" /></figure><p>Temporary string buffers are everywhere in C code.</p><p>We allocate them to build log messages, join paths, format JSON, construct SQL fragments, or prepare protocol messages. Most of the time, the code looks harmless:</p><pre>char *buf = malloc(strlen(s1) + strlen(s2) + 1);<br>strcpy(buf, s1);<br>strcat(buf, s2);<br>/* use buf */<br>free(buf);</pre><p>It is simple, but not free.</p><p>Even when the strings are small, this pattern still pays for:</p><ul><li>heap allocation</li><li>heap free</li><li>allocator bookkeeping</li><li>potential fragmentation over time</li><li>In hot code paths, these costs add up.</li></ul><p>C has an underutilized feature that can be very useful: Variable Length Arrays (VLA). It is possible to use VLA for small temporary strings, and fall back to malloc only when the buffer becomes larger than some threshold.</p><p>VLAs were introduced in C99, and are supported by all major Linux compilers (GCC, Clang, …). They are often overlooked today, but in carefully bounded situations they can remove unnecessary heap allocations and improve performance.</p><p>A complete runnable example is available on <a href="https://gist.github.com/yairlenga/ad97b1136dbb0877dde2a5b8f40853f4">GitHub Gist</a>:</p><h3>The idea</h3><p>The strategy is simple:</p><ul><li>small temporary string → allocate on the stack</li><li>large temporary string → allocate on the heap</li></ul><p>This gives a useful hybrid behavior:</p><pre>Size   Allocation      Mode       VLA<br>small  stack          (fast)      YES<br>large  heap           (safe)      NO</pre><p>Stack allocation is extremely cheap — usually just adjusting the stack pointer — while malloc requires allocator bookkeeping.</p><h3>Few helper macros</h3><p>I wrapped the pattern in a small set of macros.</p><pre>  // Set default threshold to choose between stack and heap<br>  // Override with -D, or by doing #define before using<br><br>#ifndef FLEX_STR_MAX<br>#define FLEX_STR_MAX 64<br>#endif<br><br>  // Create string variable named &#39;var_&#39; pointing to VLA OR malloced<br>  // memory based on the size. If using malloced, VLA size is 0.<br>  // See below for explanation of the hidden variables (var_##_sz,<br>  // and var_##_vla.<br><br>#define FLEX_STR_INIT(var_, sz_) \<br>  int var_##_sz = sz_ ; \<br>  char var_##_vla[var_##_sz &gt;FLEX_STR_MAX ? 0 : var_##_sz] ; \<br>  char *var_ = sizeof(var_##_vla) ? var_##_vla : malloc(var_##_sz)<br><br>  // Free (if needed) the memory associated with var_. NO-OP is vla<br>  // was used.<br><br>#define FLEX_STR_FREE(var_) \<br>  if ( !sizeof(var_##_vla) ) { free(var_) ; var_ = NULL ; }<br><br>  // Access the size of the allocated buffer<br>#define FLEX_STR_SIZE(var_) ((int) (var_##_sz))<br><br>  // Construct two parameters buf, sizeof(buf) for function calls<br>  // e.g., snprintf, etc.<br>#define FLEX_STR_BUF(var_) (var_),FLEX_STR_SIZE(var_)</pre><p>The macros creates 2 helper variable for each temporary storage variable var. The first, named var##_sz stores the actual size of the buffer so that other functions (malloc, snprintf) can retrieve the available string capacity. The second var##_vla is the VLA buffer (size 0 if not needed).</p><p>Usage is straightforward:</p><pre>// Create/Allocate<br>// result will point to stack or heap buffer, based on size_limit<br>FLEX_STR_INIT(result, size_limit);<br><br>...<br>// use result, guarenteed have enough space as per INIT<br>// use FLEX_STR_SIZE(result) to retrieve actual size from init.<br>printf(&quot;Result(sz=%d)=%s\n&quot;, FLEX_STR_SIZE(result), result) ;<br><br>// When done ...<br>FLEX_STR_FREE(result);</pre><p>If the required size is small enough, the buffer lives on the stack. Otherwise it comes from malloc.</p><p>The calling code does not need to care which one was used.</p><h4>Safe and efficient concatenation</h4><p>For this kind of operation I avoid strcat or strlcat, because those functions scan the destination buffer to find its end. Since the result buffer is fresh, we already know exactly what we want to copy.</p><p>A simple helper like this works well:</p><pre>static inline void concat(char *result, int sz, const char *s1, const char *s2)<br>{<br>  int l1 = strlen(s1) ; if ( l1 &gt;= sz ) l1=sz-1 ;<br>  memcpy(result, s1, l1) ; result += l1 ; sz -= l1 ;<br>  int l2 = strlen(s2) ; if ( l2 &gt;= sz ) l2=sz-1 ;<br>  memcpy(result, s2, l2) ;<br>  result[l2] = 0 ;<br>}</pre><p>Example usage:</p><pre>static void test1(bool show, const char *s1, const char *s2)<br>{<br>  FLEX_STR_INIT(result, strlen(s1) + strlen(s2) + 1);<br>  concat(FLEX_STR_BUF(result), s1, s2)) ;<br>  if (show) printf(&quot;result(%zu)=%s\n&quot;, FLEX_STR_SIZE(result), result);<br>  FLEX_STR_FREE(result);<br>}</pre><h3>A quick benchmark</h3><p>To get a rough idea of the impact, I ran a simple microbenchmark. The test concatenates two strings, with stack/heap threshold set at 64 bytes:</p><ul><li>First is about 60 bytes</li><li>Second is either 3 or 5 bytes (to trigger switch stack/heap).</li><li>repeated 1 million times</li></ul><p>Results (CPU time) on my system (AMD Ryzen 5 7640H), Ubuntu/WSL.</p><pre>Method          Optimized(-O)  Debug (-g)    Time (-Ofast)<br>VLA/Stack       0.013 sec      0.015 (sec)    0.009 (sec)<br>malloc/free     0.021 sec      0.023 (sec)    0.014 (sec)</pre><p>This represents roughly 35% reduction in total runtime (<strong>or one half speedup)</strong>. Another way to interpret the number — each 1M of malloc/free pairs add ~0.005 seconds to the execution time.</p><p>This is only a microbenchmark — real workloads will vary — but it shows that allocator overhead is not negligible even for fairly small operations.</p><h4>Why this helps</h4><p>Stack allocation is extremely cheap. In many cases it compiles down to something like:</p><pre>sub rsp, N</pre><p>while malloc/free involves:</p><ul><li>function calls — malloc and free</li><li>metadata updates</li><li>possible locking</li><li>heap bookkeeping</li></ul><p>Avoiding heap traffic for small temporary buffers reduces both CPU overhead and heap churn.</p><h4>When this pattern is useful</h4><p>This technique is most helpful in code that creates many short-lived strings:</p><ul><li>logging</li><li>JSON generation</li><li>File Paths &amp; URL manipulation</li><li>command construction</li><li>protocol formatting</li></ul><p>In these situations, most buffers are small and temporary, so the stack path becomes the common case.</p><h3>When VLAs are NOT appropriate</h3><p>VLAs are useful for small temporary buffers, but they should not be used in every situation.</p><p>Avoid VLAs when:</p><ul><li>The size may will exceed available stack space <strong>most </strong>of the time.</li><li>The function is (deeply) <strong>recursive</strong></li><li>The buffer must outlive the function.</li></ul><p>For these cases, heap allocations remains the safer choice.</p><h3>Caveats</h3><p>A few caveats are worth mentioning.</p><p><strong>Stack space is limited</strong></p><p>On a single-threaded Linux program, a VLA of a few hundred kilobytes may be acceptable. The default stack size on many modern Linux systems (for example Ubuntu) is typically around <strong>8 MB — </strong>and can be configured based on the available memory. Therefore, VLA of 100KB or even 512KB can be acceptable.</p><p>In multi-threaded programs, however, each thread has its own stack, and the default stack size <strong>MAY </strong>be smaller — This can be configured on a per-process, or even per thread. On x86–64–8MB is typical default, but upper limits will be usually lower, depending on the number of threads.</p><p>On embedded systems, stack sizes can be dramatically smaller, sometimes <strong>only a few kilobytes</strong>.</p><p>Because of this, VLAs should generally be limited to <strong>reasonable, bounded buffers</strong>, which is why the helper macro in this article falls back to malloc once the requested size exceeds a configurable threshold.</p><h4>Not all compilers/environments support VLAs</h4><p>They were introduced in C99 and are supported by GCC and Clang, but Microsoft Visual C does not implement them (to my best of my research).</p><p>Also wanted to highlight that the examples above use GCC13/Clang extensions. Code is not meant to be ISO compliant.</p><h4>Stack buffers cannot escape their scope</h4><p>If the buffer came from the stack, it becomes invalid when the function returns. If a some strings need a life-time beyond a function life time — use (conditional) strdup to move those strings to the heap/static or global storage.</p><h3>Final thoughts</h3><p>This is not a framework a library or a new language feature. It is simply a small pattern:</p><ul><li>stack for small buffers</li><li>heap for large buffers</li><li>minimal code overhead</li></ul><p>In the right places, that can reduce allocator traffic, avoid fragmentation pressure, and make temporary string handling more efficient.</p><p>Sometimes the fastest memory allocator is simply the stack.</p><h3>Discussion</h3><p>Do you use VLAs in production code, or are they avoided in your codebase?</p><p>I’m curious how other C developers handle temporary string buffers in performance-sensitive code.</p><h4>Complete Example</h4><p>A complete runnable example is available on <a href="https://gist.github.com/yairlenga/ad97b1136dbb0877dde2a5b8f40853f4">GitHub Gist</a>: build/run instructions in commentd on the top of the text</p><h3>Follow up article</h3><p>Medium (no paywall): <a href="https://medium.com/@yair.lenga/how-much-stack-space-do-you-have-estimating-remaining-stack-in-c-on-linux-3c9513beabd8?postPublishedType=repub">How Much Stack Space Do You Have? Estimating Remaining Stack in C on Linux</a></p><h3>Disclaimer</h3><p>The views expressed in this article are my own and do not necessarily reflect those of my employer.</p><p>Some of the code examples in this article were generated with the assistance of AI tools and have not been tested in production environments. They are provided for illustration and experimentation only.</p><p>Unless otherwise noted, the code snippets may be used freely for any purpose without warranty of any kind.</p><blockquote><em>If this article was useful, </em><strong><em>please clap so other C developers can find it.</em></strong></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7b1fbcae7193" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/avoiding-malloc-for-small-strings-in-c-with-variable-length-arrays-vlas-7b1fbcae7193">Avoiding malloc for Small Strings in C With Variable Length Arrays (VLAs)</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>